Compare commits
10 Commits
cead54c32e
...
3ff457d498
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ff457d498 | |||
| a54d3cebf7 | |||
| f1fa3ef780 | |||
| 2bf97defcd | |||
| 42645a31ea | |||
| 56e10d9983 | |||
| dd97019264 | |||
| ea2b2c457c | |||
| c92a8e6e0b | |||
| 0ca3a5b8f1 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bin/** filter=lfs diff=lfs merge=lfs -text
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
/build
|
||||
/bin
|
||||
/assets
|
||||
/.idea
|
||||
/cmake-build-debug
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
cmake_minimum_required (VERSION 3.8)
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project ("vulkan_engine")
|
||||
project(vulkan_engine LANGUAGES C CXX)
|
||||
|
||||
if (WIN32)
|
||||
set(VULKAN_SDK "$ENV{VULKAN_SDK}")
|
||||
set(Vulkan_INCLUDE_DIR "C:/VulkanSDK/1.3.296.0/Include")
|
||||
set(Vulkan_LIBRARY "C:/VulkanSDK/1.3.296.0/Lib/vulkan-1.lib")
|
||||
if(WIN32)
|
||||
if(DEFINED ENV{VULKAN_SDK})
|
||||
file(TO_CMAKE_PATH "$ENV{VULKAN_SDK}" _VULKAN_SDK)
|
||||
set(Vulkan_INCLUDE_DIR "${_VULKAN_SDK}/Include")
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(Vulkan_LIBRARY "${_VULKAN_SDK}/Lib/vulkan-1.lib")
|
||||
else()
|
||||
set(Vulkan_LIBRARY "${_VULKAN_SDK}/Lib32/vulkan-1.lib")
|
||||
endif()
|
||||
elseif(EXISTS "C:/VulkanSDK/1.3.296.0/Include")
|
||||
# Fallback for a common SDK install path; update if your SDK version differs.
|
||||
set(Vulkan_INCLUDE_DIR "C:/VulkanSDK/1.3.296.0/Include")
|
||||
set(Vulkan_LIBRARY "C:/VulkanSDK/1.3.296.0/Lib/vulkan-1.lib")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(Vulkan REQUIRED)
|
||||
find_package(Vulkan REQUIRED COMPONENTS glslangValidator)
|
||||
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
|
||||
|
||||
# Third-party deps are vendored; keep builds offline-friendly by default.
|
||||
# BVH2's CMake enables tests by default, which would FetchContent googletest.
|
||||
@@ -16,44 +30,46 @@ set(BVH2_ENABLE_TESTS OFF CACHE BOOL "Disable BVH2 tests (offline builds)" FORCE
|
||||
|
||||
add_subdirectory(third_party)
|
||||
|
||||
if (MSVC AND CMAKE_C_COMPILER_ID MATCHES "Clang")
|
||||
if (TARGET SDL2)
|
||||
target_link_options(SDL2 PRIVATE /DEFAULTLIB:ucrt.lib /DEFAULTLIB:vcruntime.lib)
|
||||
endif()
|
||||
if (TARGET SDL2-static)
|
||||
target_link_options(SDL2-static PRIVATE /DEFAULTLIB:ucrt.lib /DEFAULTLIB:vcruntime.lib)
|
||||
endif()
|
||||
if(MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_SIMULATE_ID STREQUAL "MSVC")
|
||||
foreach(_SDL_TARGET IN ITEMS SDL2 SDL2-static)
|
||||
if(TARGET ${_SDL_TARGET})
|
||||
target_link_options(${_SDL_TARGET} PRIVATE /DEFAULTLIB:ucrt.lib /DEFAULTLIB:vcruntime.lib)
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
|
||||
set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin")
|
||||
|
||||
add_subdirectory(src)
|
||||
|
||||
find_program(GLSL_VALIDATOR glslangValidator HINTS /usr/bin /usr/local/bin $ENV{VULKAN_SDK}/Bin/ $ENV{VULKAN_SDK}/Bin32/)
|
||||
option(VULKAN_ENGINE_BUILD_SHADERS "Compile GLSL shaders to SPIR-V at build time" ON)
|
||||
|
||||
if(VULKAN_ENGINE_BUILD_SHADERS)
|
||||
set(GLSL_VALIDATOR "${Vulkan_GLSLANG_VALIDATOR_EXECUTABLE}")
|
||||
if(NOT GLSL_VALIDATOR)
|
||||
find_program(GLSL_VALIDATOR glslangValidator HINTS /usr/bin /usr/local/bin)
|
||||
endif()
|
||||
|
||||
if(NOT GLSL_VALIDATOR)
|
||||
message(FATAL_ERROR "glslangValidator not found. Install the Vulkan SDK or disable shader compilation with -DVULKAN_ENGINE_BUILD_SHADERS=OFF.")
|
||||
endif()
|
||||
|
||||
|
||||
file(GLOB_RECURSE GLSL_SOURCE_FILES
|
||||
"${PROJECT_SOURCE_DIR}/shaders/*.frag"
|
||||
"${PROJECT_SOURCE_DIR}/shaders/*.vert"
|
||||
"${PROJECT_SOURCE_DIR}/shaders/*.comp"
|
||||
file(GLOB_RECURSE GLSL_SOURCE_FILES CONFIGURE_DEPENDS
|
||||
"${PROJECT_SOURCE_DIR}/shaders/*.frag"
|
||||
"${PROJECT_SOURCE_DIR}/shaders/*.vert"
|
||||
"${PROJECT_SOURCE_DIR}/shaders/*.comp"
|
||||
)
|
||||
|
||||
foreach(GLSL ${GLSL_SOURCE_FILES})
|
||||
message(STATUS "BUILDING SHADER")
|
||||
get_filename_component(FILE_NAME ${GLSL} NAME)
|
||||
set(SPIRV "${PROJECT_SOURCE_DIR}/shaders/${FILE_NAME}.spv")
|
||||
message(STATUS ${GLSL})
|
||||
add_custom_command(
|
||||
OUTPUT ${SPIRV}
|
||||
COMMAND ${GLSL_VALIDATOR} -V --target-env vulkan1.2 ${GLSL} -o ${SPIRV}
|
||||
DEPENDS ${GLSL})
|
||||
list(APPEND SPIRV_BINARY_FILES ${SPIRV})
|
||||
endforeach(GLSL)
|
||||
set(SPIRV_BINARY_FILES "")
|
||||
foreach(GLSL IN LISTS GLSL_SOURCE_FILES)
|
||||
set(SPIRV "${GLSL}.spv")
|
||||
add_custom_command(
|
||||
OUTPUT "${SPIRV}"
|
||||
COMMAND "${GLSL_VALIDATOR}" -V --target-env vulkan1.2 "${GLSL}" -o "${SPIRV}"
|
||||
DEPENDS "${GLSL}"
|
||||
VERBATIM
|
||||
)
|
||||
list(APPEND SPIRV_BINARY_FILES "${SPIRV}")
|
||||
endforeach()
|
||||
|
||||
add_custom_target(shaders_spirv DEPENDS ${SPIRV_BINARY_FILES})
|
||||
|
||||
# Ensure shaders are built alongside the executable.
|
||||
add_dependencies(vulkan_engine shaders_spirv)
|
||||
add_custom_target(shaders_spirv DEPENDS ${SPIRV_BINARY_FILES})
|
||||
add_dependencies(vulkan_engine shaders_spirv)
|
||||
endif()
|
||||
|
||||
BIN
bin/ImGuizmo.lib
LFS
Normal file
BIN
bin/ImGuizmo.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/ImGuizmo.lib
LFS
Normal file
BIN
bin/Release/ImGuizmo.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/SDL2.dll
LFS
Normal file
BIN
bin/Release/SDL2.dll
LFS
Normal file
Binary file not shown.
BIN
bin/Release/SDL2.exp
LFS
Normal file
BIN
bin/Release/SDL2.exp
LFS
Normal file
Binary file not shown.
BIN
bin/Release/SDL2.lib
LFS
Normal file
BIN
bin/Release/SDL2.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/fastgltf.lib
LFS
Normal file
BIN
bin/Release/fastgltf.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/fastgltf_simdjson.lib
LFS
Normal file
BIN
bin/Release/fastgltf_simdjson.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/fmt.lib
LFS
Normal file
BIN
bin/Release/fmt.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/imgui.lib
LFS
Normal file
BIN
bin/Release/imgui.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/ktx.dll
LFS
Normal file
BIN
bin/Release/ktx.dll
LFS
Normal file
Binary file not shown.
BIN
bin/Release/mikktspace.lib
LFS
Normal file
BIN
bin/Release/mikktspace.lib
LFS
Normal file
Binary file not shown.
BIN
bin/Release/vkbootstrap.lib
LFS
Normal file
BIN
bin/Release/vkbootstrap.lib
LFS
Normal file
Binary file not shown.
BIN
bin/SDL2.dll
LFS
Normal file
BIN
bin/SDL2.dll
LFS
Normal file
Binary file not shown.
BIN
bin/SDL2.lib
LFS
Normal file
BIN
bin/SDL2.lib
LFS
Normal file
Binary file not shown.
BIN
bin/SDL2d.dll
LFS
Normal file
BIN
bin/SDL2d.dll
LFS
Normal file
Binary file not shown.
BIN
bin/SDL2d.lib
LFS
Normal file
BIN
bin/SDL2d.lib
LFS
Normal file
Binary file not shown.
BIN
bin/SDL2d.pdb
LFS
Normal file
BIN
bin/SDL2d.pdb
LFS
Normal file
Binary file not shown.
BIN
bin/dwd.rdc
LFS
Normal file
BIN
bin/dwd.rdc
LFS
Normal file
Binary file not shown.
BIN
bin/engine.exe
LFS
Normal file
BIN
bin/engine.exe
LFS
Normal file
Binary file not shown.
BIN
bin/engine.pdb
LFS
Normal file
BIN
bin/engine.pdb
LFS
Normal file
Binary file not shown.
BIN
bin/fastgltf.lib
LFS
Normal file
BIN
bin/fastgltf.lib
LFS
Normal file
Binary file not shown.
BIN
bin/fastgltf_simdjson.lib
LFS
Normal file
BIN
bin/fastgltf_simdjson.lib
LFS
Normal file
Binary file not shown.
BIN
bin/fmt.lib
LFS
Normal file
BIN
bin/fmt.lib
LFS
Normal file
Binary file not shown.
BIN
bin/fmtd.lib
LFS
Normal file
BIN
bin/fmtd.lib
LFS
Normal file
Binary file not shown.
BIN
bin/imgui.ini
LFS
Normal file
BIN
bin/imgui.ini
LFS
Normal file
Binary file not shown.
BIN
bin/imgui.lib
LFS
Normal file
BIN
bin/imgui.lib
LFS
Normal file
Binary file not shown.
BIN
bin/ktx.dll
LFS
Normal file
BIN
bin/ktx.dll
LFS
Normal file
Binary file not shown.
BIN
bin/libImGuizmo.a
LFS
Normal file
BIN
bin/libImGuizmo.a
LFS
Normal file
Binary file not shown.
1
bin/libSDL2-2.0d.so
Symbolic link
1
bin/libSDL2-2.0d.so
Symbolic link
@@ -0,0 +1 @@
|
||||
libSDL2-2.0d.so.0
|
||||
1
bin/libSDL2-2.0d.so.0
Symbolic link
1
bin/libSDL2-2.0d.so.0
Symbolic link
@@ -0,0 +1 @@
|
||||
libSDL2-2.0d.so.0.3200.10
|
||||
BIN
bin/libSDL2-2.0d.so.0.3200.10
LFS
Normal file
BIN
bin/libSDL2-2.0d.so.0.3200.10
LFS
Normal file
Binary file not shown.
BIN
bin/libfastgltf.a
LFS
Normal file
BIN
bin/libfastgltf.a
LFS
Normal file
Binary file not shown.
BIN
bin/libfastgltf_simdjson.a
LFS
Normal file
BIN
bin/libfastgltf_simdjson.a
LFS
Normal file
Binary file not shown.
BIN
bin/libfmtd.a
LFS
Normal file
BIN
bin/libfmtd.a
LFS
Normal file
Binary file not shown.
BIN
bin/libimgui.a
LFS
Normal file
BIN
bin/libimgui.a
LFS
Normal file
Binary file not shown.
BIN
bin/libmikktspace.a
LFS
Normal file
BIN
bin/libmikktspace.a
LFS
Normal file
Binary file not shown.
BIN
bin/libvkbootstrap.a
LFS
Normal file
BIN
bin/libvkbootstrap.a
LFS
Normal file
Binary file not shown.
BIN
bin/mikktspace.lib
LFS
Normal file
BIN
bin/mikktspace.lib
LFS
Normal file
Binary file not shown.
BIN
bin/vkbootstrap.lib
LFS
Normal file
BIN
bin/vkbootstrap.lib
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_AssetManager.json
LFS
Normal file
BIN
bin/vma_after_AssetManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_Compute.json
LFS
Normal file
BIN
bin/vma_after_Compute.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_MainDQ.json
LFS
Normal file
BIN
bin/vma_after_MainDQ.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_PipelineManager.json
LFS
Normal file
BIN
bin/vma_after_PipelineManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_RTManager.json
LFS
Normal file
BIN
bin/vma_after_RTManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_RenderPassManager.json
LFS
Normal file
BIN
bin/vma_after_RenderPassManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_ResourceManager.json
LFS
Normal file
BIN
bin/vma_after_ResourceManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_Samplers_Descriptors.json
LFS
Normal file
BIN
bin/vma_after_Samplers_Descriptors.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_SceneManager.json
LFS
Normal file
BIN
bin/vma_after_SceneManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_after_Swapchain.json
LFS
Normal file
BIN
bin/vma_after_Swapchain.json
LFS
Normal file
Binary file not shown.
BIN
bin/vma_before_DeviceManager.json
LFS
Normal file
BIN
bin/vma_before_DeviceManager.json
LFS
Normal file
Binary file not shown.
BIN
bin/vulkan_engine
LFS
Normal file
BIN
bin/vulkan_engine
LFS
Normal file
Binary file not shown.
BIN
bin/vulkan_engine.exe
LFS
Normal file
BIN
bin/vulkan_engine.exe
LFS
Normal file
Binary file not shown.
BIN
bin/vulkan_engine.ilk
LFS
Normal file
BIN
bin/vulkan_engine.ilk
LFS
Normal file
Binary file not shown.
BIN
bin/vulkan_engine.pdb
LFS
Normal file
BIN
bin/vulkan_engine.pdb
LFS
Normal file
Binary file not shown.
@@ -2,7 +2,7 @@
|
||||
|
||||
- Prerequisites
|
||||
- Vulkan SDK installed and `VULKAN_SDK` set.
|
||||
- A C++20 compiler and CMake ≥ 3.8.
|
||||
- A C++20 compiler and CMake ≥ 3.16.
|
||||
- GPU drivers with Vulkan 1.2+.
|
||||
- KTX software with libktx
|
||||
|
||||
|
||||
@@ -7,29 +7,52 @@ Lightweight render graph that builds a per‑frame DAG from pass declarations, c
|
||||
- Centralize synchronization and image layout transitions across passes.
|
||||
- Make passes declarative: author declares reads/writes; the graph inserts barriers and begins/ends rendering.
|
||||
- Keep existing pass classes (`IRenderPass`) while migrating execution to the graph.
|
||||
- Provide runtime profiling and debugging capabilities for pass execution.
|
||||
|
||||
### High‑Level Flow
|
||||
|
||||
- Engine creates the graph each frame and imports swapchain/G‑Buffer images: `src/core/engine.cpp:303`.
|
||||
- Engine creates the graph each frame and imports swapchain/G‑Buffer images: `src/core/engine.cpp`.
|
||||
- Each pass registers its work by calling `register_graph(graph, ...)` and declaring resources via a builder.
|
||||
- The graph appends a present chain (copy HDR `drawImage` → swapchain, then transition to `PRESENT`), optionally inserting ImGui before present.
|
||||
- `compile()` topologically sorts passes by data dependencies (read/write) and computes per‑pass barriers.
|
||||
- `execute(cmd)` emits barriers, begins dynamic rendering if attachments were declared, calls the pass record lambda, and ends rendering.
|
||||
- `compile()` topologically sorts passes by data dependencies (read/write hazards: RAW/WAW/WAR) and computes per‑pass barriers using `VkDependencyInfo` with `Vk*MemoryBarrier2`.
|
||||
- `execute(cmd)` creates timestamp query pools, emits barriers, begins dynamic rendering if attachments were declared, calls the pass record lambda, ends rendering, and records GPU/CPU timings.
|
||||
- `resolve_timings()` retrieves GPU timestamp results after the fence is signaled, converting them to milliseconds.
|
||||
|
||||
### Core API
|
||||
|
||||
**Lifecycle:**
|
||||
- `RenderGraph::init(ctx)` — Initialize with engine context. See `src/render/graph/graph.cpp:28`.
|
||||
- `RenderGraph::clear()` — Clear all passes and reset resources. See `src/render/graph/graph.cpp:34`.
|
||||
- `RenderGraph::shutdown()` — Destroy GPU resources (query pools) before device shutdown. See `src/render/graph/graph.cpp:40`.
|
||||
|
||||
**Pass Registration:**
|
||||
- `RenderGraph::add_pass(name, RGPassType type, BuildCallback build, RecordCallback record)`
|
||||
- Declare image/buffer accesses and attachments inside `build` using `RGPassBuilder`.
|
||||
- Do your actual rendering/copies in `record` using resolved Vulkan objects from `RGPassResources`.
|
||||
- See: `src/render/graph/graph.h:36`, `src/render/graph/graph.cpp:51`.
|
||||
- See: `src/render/graph/graph.h:42`, `src/render/graph/graph.cpp:91`.
|
||||
- Legacy form: `add_pass(name, type, record)` for passes with no resource declarations. See `src/render/graph/graph.cpp:117`.
|
||||
|
||||
- `RenderGraph::compile()` → builds ordering and per‑pass `Vk*MemoryBarrier2` lists. See `src/render/graph/graph.cpp:83`.
|
||||
**Resource Creation:**
|
||||
- `import_image(desc)` / `import_buffer(desc)` — Import externally owned resources (deduplicated by VkImage/VkBuffer handle).
|
||||
- `create_image(desc)` / `create_buffer(desc)` — Create transient resources (destroyed at end of frame via deletion queue).
|
||||
- `create_depth_image(name, extent, format=D32_SFLOAT)` — Convenience helper for depth-only images with depth attachment + sampled usage. See `src/render/graph/graph.cpp:67`.
|
||||
|
||||
- `RenderGraph::execute(cmd)` → emits barriers and dynamic rendering begin/end. See `src/render/graph/graph.cpp:592`.
|
||||
**Compilation and Execution:**
|
||||
- `RenderGraph::compile()` — Build topological ordering (Kahn's algorithm) and per‑pass `VkImageMemoryBarrier2` / `VkBufferMemoryBarrier2` lists. Returns false on error. See `src/render/graph/graph.cpp:123`.
|
||||
- `RenderGraph::execute(cmd)` — Creates timestamp query pool, emits barriers via `vkCmdPipelineBarrier2`, begins dynamic rendering if attachments exist, invokes record callbacks, ends rendering, and writes GPU timestamps. See `src/render/graph/graph.cpp:874`.
|
||||
- `RenderGraph::resolve_timings()` — Fetch GPU timestamp results after fence wait and convert to milliseconds. Must be called before next `execute()`. See `src/render/graph/graph.cpp:1314`.
|
||||
|
||||
- Import helpers for engine images: `import_draw_image()`, `import_depth_image()`, `import_gbuffer_*()`, `import_swapchain_image(index)`. See `src/render/graph/graph.cpp:740`.
|
||||
**Import Helpers:**
|
||||
- `import_draw_image()`, `import_depth_image()`, `import_gbuffer_position()`, `import_gbuffer_normal()`, `import_gbuffer_albedo()`, `import_gbuffer_extra()`, `import_id_buffer()`, `import_swapchain_image(index)` — Convenience wrappers for engine-owned images. See `src/render/graph/graph.cpp:1147–1312`.
|
||||
|
||||
- Present chain: `add_present_chain(draw, swapchain, appendExtra)` inserts Copy→Present passes and lets you inject extra passes (e.g., ImGui) in between. See `src/render/graph/graph.cpp:705`.
|
||||
**Present Chain:**
|
||||
- `add_present_chain(draw, swapchain, appendExtra)` — Inserts `PresentLetterbox` pass (blit draw→swapchain with letterboxing) and `PreparePresent` pass (layout transition to `PRESENT_SRC_KHR`). Optional `appendExtra` callback injects passes (e.g., ImGui) in between. See `src/render/graph/graph.cpp:1043`.
|
||||
|
||||
**Debug and Profiling:**
|
||||
- `pass_count()`, `pass_name(i)`, `pass_enabled(i)`, `set_pass_enabled(i, enabled)` — Runtime pass enable/disable. See `src/render/graph/graph.h:105–108`.
|
||||
- `debug_get_passes(out)` — Retrieve pass metadata including GPU/CPU timings, resource access counts, attachment info. See `src/render/graph/graph.cpp:1163`.
|
||||
- `debug_get_images(out)` — Retrieve image metadata (imported/transient, format, extent, usage, lifetime). See `src/render/graph/graph.cpp:1186`.
|
||||
- `debug_get_buffers(out)` — Retrieve buffer metadata. See `src/render/graph/graph.cpp:1207`.
|
||||
|
||||
### Declaring a Pass
|
||||
|
||||
@@ -65,63 +88,155 @@ void MyPass::register_graph(RenderGraph* graph,
|
||||
|
||||
### Builder Reference (`RGPassBuilder`)
|
||||
|
||||
- Images
|
||||
- `read(RGImageHandle, RGImageUsage)` → sample/read usage for this pass.
|
||||
- `write(RGImageHandle, RGImageUsage)` → write usage (compute/storage/transfer).
|
||||
- `write_color(RGImageHandle, bool clearOnLoad=false, VkClearValue clear={})` → declares a color attachment.
|
||||
- `write_depth(RGImageHandle, bool clearOnLoad=false, VkClearValue clear={})` → declares a depth attachment.
|
||||
Passed to the `BuildCallback` to declare resource accesses and attachments. See `src/render/graph/builder.h:40`.
|
||||
|
||||
- Buffers
|
||||
- `read_buffer(RGBufferHandle, RGBufferUsage)` / `write_buffer(RGBufferHandle, RGBufferUsage)`.
|
||||
- Convenience import: `read_buffer(VkBuffer, RGBufferUsage, size, name)` and `write_buffer(VkBuffer, ...)` dedup by raw handle.
|
||||
**Image Access:**
|
||||
- `read(RGImageHandle, RGImageUsage)` — Declare sampled/read usage (e.g., `SampledFragment`, `TransferSrc`). See `src/render/graph/builder.cpp:20`.
|
||||
- `write(RGImageHandle, RGImageUsage)` — Declare write usage (e.g., `ComputeWrite`, `TransferDst`). See `src/render/graph/builder.cpp:25`.
|
||||
- `write_color(RGImageHandle, bool clearOnLoad=false, VkClearValue clear={})` — Declare color attachment with optional clear. Sets usage to `ColorAttachment` and `store=true` by default. See `src/render/graph/builder.cpp:30`.
|
||||
- `write_depth(RGImageHandle, bool clearOnLoad=false, VkClearValue clear={})` — Declare depth attachment with optional clear. See `src/render/graph/builder.cpp:40`.
|
||||
|
||||
See `src/render/graph/builder.h:39` and impl in `src/render/graph/builder.cpp:20`.
|
||||
**Buffer Access:**
|
||||
- `read_buffer(RGBufferHandle, RGBufferUsage)` — Declare buffer read (e.g., `VertexRead`, `IndexRead`, `UniformRead`, `StorageRead`). See `src/render/graph/builder.cpp:50`.
|
||||
- `write_buffer(RGBufferHandle, RGBufferUsage)` — Declare buffer write (e.g., `StorageReadWrite`, `TransferDst`). See `src/render/graph/builder.cpp:55`.
|
||||
- Convenience overloads: `read_buffer(VkBuffer, RGBufferUsage, size, name)` and `write_buffer(VkBuffer, ...)` automatically import and deduplicate by raw `VkBuffer` handle. See `src/render/graph/builder.cpp:60,70`.
|
||||
|
||||
**Resource Resolution (`RGPassResources`):**
|
||||
Used inside the `RecordCallback` to fetch resolved Vulkan objects. See `src/render/graph/builder.h:22`.
|
||||
- `image(RGImageHandle)` → `VkImage`
|
||||
- `image_view(RGImageHandle)` → `VkImageView`
|
||||
- `buffer(RGBufferHandle)` → `VkBuffer`
|
||||
|
||||
### Resource Model (`RGResourceRegistry`)
|
||||
|
||||
- Imported vs transient resources are tracked uniformly with lifetime indices (`firstUse/lastUse`).
|
||||
- Imports are deduplicated by `VkImage`/`VkBuffer` and keep initial layout/stage/access as the starting state.
|
||||
- Transients are created via `ResourceManager` and auto‑destroyed at end of frame using the frame deletion queue.
|
||||
- See `src/render/graph/resources.h:11` and `src/render/graph/resources.cpp:1`.
|
||||
Manages both imported (externally owned) and transient (graph-owned) resources. See `src/render/graph/resources.h:52`.
|
||||
|
||||
**Imported Resources:**
|
||||
- Deduplicated by raw Vulkan handle (`VkImage`/`VkBuffer`) using hash maps (`_imageLookup`/`_bufferLookup`). See `src/render/graph/resources.cpp`.
|
||||
- Initial layout/stage/access preserved from `RGImportedImageDesc`/`RGImportedBufferDesc`.
|
||||
- Ownership remains external; graph does not destroy these resources.
|
||||
|
||||
**Transient Resources:**
|
||||
- Created via `ResourceManager` (`AllocatedImage`/`AllocatedBuffer`) with VMA allocations. See `src/render/graph/resources.cpp`.
|
||||
- Automatically destroyed at end of frame via frame deletion queue.
|
||||
- Usage flags must cover all declared usages (validated during `compile()`).
|
||||
|
||||
**Lifetime Tracking:**
|
||||
- `firstUse` and `lastUse` indices computed during `compile()` (see `src/render/graph/graph.cpp:854–869`).
|
||||
- Used for debug visualization and future aliasing/pooling optimizations.
|
||||
|
||||
**Records (`RGImageRecord`/`RGBufferRecord`):**
|
||||
Unified representation storing `VkImage`/`VkBuffer`, `VkImageView`, format, extent, initial state, and allocation info. See `src/render/graph/resources.h:11,34`.
|
||||
|
||||
### Synchronization and Layouts
|
||||
|
||||
- For each pass, `compile()` compares previous state with desired usage and, if needed, adds a pre‑pass barrier:
|
||||
- Images: `VkImageMemoryBarrier2` with stage/access/layout derived from `RGImageUsage`.
|
||||
- Buffers: `VkBufferMemoryBarrier2` with stage/access derived from `RGBufferUsage`.
|
||||
- Initial state comes from the imported descriptor; if unknown, buffers default to `TOP_OF_PIPE`.
|
||||
- Format/usage checks:
|
||||
- Warns if binding a depth format as color (and vice‑versa).
|
||||
- Warns if a transient resource is used with flags it wasn’t created with.
|
||||
**Barrier Generation (see `src/render/graph/graph.cpp:232–851`):**
|
||||
|
||||
Image usage → layout/stage examples (subset):
|
||||
For each enabled pass, `compile()` tracks per-resource state (`ImageState`/`BufferState`) and inserts barriers when hazards are detected:
|
||||
|
||||
- `SampledFragment` → `SHADER_READ_ONLY_OPTIMAL`, `FRAGMENT_SHADER`.
|
||||
- `ColorAttachment` → `COLOR_ATTACHMENT_OPTIMAL`, `COLOR_ATTACHMENT_OUTPUT` (read|write).
|
||||
- `DepthAttachment` → `DEPTH_ATTACHMENT_OPTIMAL`, `EARLY|LATE_FRAGMENT_TESTS`.
|
||||
- `TransferDst` → `TRANSFER_DST_OPTIMAL`, `TRANSFER`.
|
||||
- `Present` → `PRESENT_SRC_KHR`, `BOTTOM_OF_PIPE`.
|
||||
**Image Barriers (`VkImageMemoryBarrier2`):**
|
||||
- Triggered by: layout change, prior write before read/write (RAW/WAW), prior reads before write (WAR).
|
||||
- Stage/access/layout derived from `RGImageUsage` via `usage_info_image()` (see `src/render/graph/graph.cpp:313–365`).
|
||||
- Aspect determined by usage and format (depth formats get `DEPTH_BIT`, others `COLOR_BIT`).
|
||||
- Initial state from `RGImportedImageDesc::currentLayout/currentStage/currentAccess`; if unknown (layout ≠ UNDEFINED but stage=NONE), conservatively assumes `ALL_COMMANDS + MEMORY_READ|WRITE`.
|
||||
|
||||
Buffer usage → stage/access examples:
|
||||
**Buffer Barriers (`VkBufferMemoryBarrier2`):**
|
||||
- Triggered by: prior write before read/write, prior reads before write.
|
||||
- Stage/access derived from `RGBufferUsage` via `usage_info_buffer()` (see `src/render/graph/graph.cpp:367–411`).
|
||||
- Size: exact size for transients, `VK_WHOLE_SIZE` for imports (to avoid validation errors).
|
||||
|
||||
- `IndexRead` → `INDEX_INPUT`, `INDEX_READ`.
|
||||
- `VertexRead` → `VERTEX_INPUT`, `VERTEX_ATTRIBUTE_READ`.
|
||||
- `UniformRead` → `ALL_GRAPHICS|COMPUTE`, `UNIFORM_READ`.
|
||||
- `StorageReadWrite` → `COMPUTE|FRAGMENT`, `SHADER_STORAGE_READ|WRITE`.
|
||||
**Usage Priority and Conflict Resolution:**
|
||||
When a pass declares multiple conflicting usages for the same resource (e.g., both `SampledFragment` and `ColorAttachment`), the graph selects the highest-priority usage for layout determination (see `image_usage_priority()` at `src/render/graph/graph.cpp:499`). Stages and access masks are unioned. Warns if layout mismatch detected.
|
||||
|
||||
**Image Usage → Layout/Stage/Access Mapping:**
|
||||
See `usage_info_image()` at `src/render/graph/graph.cpp:313`.
|
||||
|
||||
| RGImageUsage | Layout | Stage | Access |
|
||||
|---|---|---|---|
|
||||
| `SampledFragment` | `SHADER_READ_ONLY_OPTIMAL` | `FRAGMENT_SHADER` | `SHADER_SAMPLED_READ` |
|
||||
| `SampledCompute` | `SHADER_READ_ONLY_OPTIMAL` | `COMPUTE_SHADER` | `SHADER_SAMPLED_READ` |
|
||||
| `TransferSrc` | `TRANSFER_SRC_OPTIMAL` | `TRANSFER` | `TRANSFER_READ` |
|
||||
| `TransferDst` | `TRANSFER_DST_OPTIMAL` | `TRANSFER` | `TRANSFER_WRITE` |
|
||||
| `ColorAttachment` | `COLOR_ATTACHMENT_OPTIMAL` | `COLOR_ATTACHMENT_OUTPUT` | `COLOR_ATTACHMENT_READ\|WRITE` |
|
||||
| `DepthAttachment` | `DEPTH_ATTACHMENT_OPTIMAL` | `EARLY_FRAGMENT_TESTS\|LATE_FRAGMENT_TESTS` | `DEPTH_STENCIL_ATTACHMENT_READ\|WRITE` |
|
||||
| `ComputeWrite` | `GENERAL` | `COMPUTE_SHADER` | `SHADER_STORAGE_READ\|WRITE` |
|
||||
| `Present` | `PRESENT_SRC_KHR` | `BOTTOM_OF_PIPE` | `MEMORY_READ` |
|
||||
|
||||
**Buffer Usage → Stage/Access Mapping:**
|
||||
See `usage_info_buffer()` at `src/render/graph/graph.cpp:367`.
|
||||
|
||||
| RGBufferUsage | Stage | Access |
|
||||
|---|---|---|
|
||||
| `TransferSrc` | `TRANSFER` | `TRANSFER_READ` |
|
||||
| `TransferDst` | `TRANSFER` | `TRANSFER_WRITE` |
|
||||
| `VertexRead` | `VERTEX_INPUT` | `VERTEX_ATTRIBUTE_READ` |
|
||||
| `IndexRead` | `INDEX_INPUT` | `INDEX_READ` |
|
||||
| `UniformRead` | `ALL_GRAPHICS\|COMPUTE_SHADER` | `UNIFORM_READ` |
|
||||
| `StorageRead` | `COMPUTE_SHADER\|ALL_GRAPHICS` | `SHADER_STORAGE_READ` |
|
||||
| `StorageReadWrite` | `COMPUTE_SHADER\|ALL_GRAPHICS` | `SHADER_STORAGE_READ\|WRITE` |
|
||||
| `IndirectArgs` | `DRAW_INDIRECT` | `INDIRECT_COMMAND_READ` |
|
||||
|
||||
**Validation Warnings:**
|
||||
- Depth-format image declared as color attachment (or vice versa). See `src/render/graph/graph.cpp:645–657`.
|
||||
- Transient resource used without required usage flags. See `src/render/graph/graph.cpp:659–667` (images), `818–826` (buffers).
|
||||
- Multiple conflicting layouts in single pass. See `src/render/graph/graph.cpp:536–543`.
|
||||
|
||||
### Built‑In Pass Wiring (Current)
|
||||
|
||||
- Resource uploads (if any) → Background (compute) → Geometry (G‑Buffer) → Lighting (deferred) → SSR → Tonemap+Bloom → FXAA → Transparent → CopyToSwapchain → ImGui → PreparePresent.
|
||||
- See registrations in `src/core/engine.cpp`.
|
||||
|
||||
### Topological Sorting and Scheduling
|
||||
|
||||
**Dependency Graph Construction (see `src/render/graph/graph.cpp:127–231`):**
|
||||
- Reads/writes create directed edges: `writer → reader` (RAW), `writer → writer` (WAW), `reader → writer` (WAR).
|
||||
- Disabled passes are skipped during edge construction but remain in the pass list.
|
||||
- Kahn's algorithm produces a linear execution order respecting all dependencies.
|
||||
- If cycle detected (topological sort fails), falls back to insertion order but still computes barriers.
|
||||
|
||||
**Execution Order:**
|
||||
Passes execute in sorted order (or insertion order if cycle). Only enabled passes run; disabled passes are skipped during `execute()`. See `src/render/graph/graph.cpp:895`.
|
||||
|
||||
### Dynamic Rendering Setup
|
||||
|
||||
**Render Area Calculation (see `src/render/graph/graph.cpp:936–1000`):**
|
||||
- Chooses min extent across all color/depth attachments.
|
||||
- Falls back to `EngineContext::drawExtent` if no attachments.
|
||||
- Warns if color attachments have mismatched extents.
|
||||
|
||||
**Attachment Construction:**
|
||||
- Color attachments: `VkRenderingAttachmentInfo` with `clearOnLoad` → `LOAD_OP_CLEAR` / `LOAD_OP_LOAD`, `store` → `STORE_OP_STORE` / `STORE_OP_DONT_CARE`.
|
||||
- Depth attachment: similar logic; `clearValue.depthStencil` used if `clearOnLoad=true`.
|
||||
- Layout forced to `COLOR_ATTACHMENT_OPTIMAL` or `DEPTH_ATTACHMENT_OPTIMAL`.
|
||||
|
||||
See `src/render/graph/graph.cpp:927–1012`.
|
||||
|
||||
### Profiling and Timing
|
||||
|
||||
**GPU Timing (Timestamps):**
|
||||
- Per-frame `VkQueryPool` with 2 queries per pass (begin/end). Created in `execute()`, destroyed in `resolve_timings()` or next `execute()`.
|
||||
- `vkCmdWriteTimestamp2()` at `ALL_COMMANDS_BIT` stage before/after pass recording (see `src/render/graph/graph.cpp:919–923`, `1028–1032`).
|
||||
- `resolve_timings()` fetches results with `VK_QUERY_RESULT_WAIT_BIT`, converts ticks to milliseconds using `timestampPeriod`. See `src/render/graph/graph.cpp:1314–1355`.
|
||||
|
||||
**CPU Timing:**
|
||||
- `std::chrono::high_resolution_clock` measures command recording duration (`cpuStart`/`cpuEnd`). See `src/render/graph/graph.cpp:924`, `1026`.
|
||||
- Stored in `_lastCpuMillis` vector; accessible via `debug_get_passes()`.
|
||||
|
||||
**Debug Structures:**
|
||||
- `RGDebugPassInfo`: name, type, enabled, resource counts, attachment info, `gpuMillis`, `cpuMillis`. See `src/render/graph/graph.h:66`.
|
||||
- `RGDebugImageInfo`: id, name, imported, format, extent, usage, lifetime. See `src/render/graph/graph.h:83`.
|
||||
- `RGDebugBufferInfo`: id, name, imported, size, usage, lifetime. See `src/render/graph/graph.h:94`.
|
||||
|
||||
### Notes & Limits
|
||||
|
||||
- No aliasing or transient pooling yet; images created via `create_*` are released end‑of‑frame.
|
||||
- Graph scheduling uses a topological order by data dependency; it does not parallelize across queues.
|
||||
- Load/store control for attachments is minimal (`clearOnLoad`, `store` on `RGAttachmentInfo`).
|
||||
- Render area is the min of all declared attachment extents and `EngineContext::drawExtent`.
|
||||
- **No aliasing or transient pooling**: Transient images/buffers created via `create_*` are released end‑of‑frame via frame deletion queue.
|
||||
- **Single-queue execution**: Topological order is linear; no multi-queue parallelization.
|
||||
- **Minimal load/store control**: Only `clearOnLoad` and `store` flags on `RGAttachmentInfo`; no resolve or stencil control.
|
||||
- **No mid-pass barriers**: Conflicting usages within a single pass cannot be synchronized (warns but proceeds with unioned stages/access).
|
||||
- **No automatic resource aliasing**: Future work could reuse transient allocations based on lifetime non-overlap.
|
||||
|
||||
### Debugging
|
||||
|
||||
- Each pass is wrapped with a debug label (`RG: <name>`).
|
||||
- Compile prints warnings for suspicious usages or format mismatches.
|
||||
- **Per-pass debug labels**: `vkdebug::cmd_begin_label(cmd, "RG: <name>")` wraps each pass (see `src/render/graph/graph.cpp:903–906`, `1035–1038`).
|
||||
- **Compile-time validation warnings**: Printed via `fmt::println` for format mismatches, missing usage flags, layout conflicts.
|
||||
- **Runtime introspection**: Use `debug_get_*` APIs to export pass/image/buffer metadata for visualization/debugging tools.
|
||||
|
||||
@@ -22,15 +22,22 @@ auto chairPath = assets->modelPath("models/chair.glb");
|
||||
- Paths
|
||||
- `std::string shaderPath(std::string_view)`
|
||||
- `std::string assetPath(std::string_view)` / `modelPath(std::string_view)`
|
||||
- `const AssetPaths& paths() const` / `void setPaths(const AssetPaths &p)` — get/set asset paths
|
||||
- glTF
|
||||
- `std::optional<std::shared_ptr<LoadedGLTF>> loadGLTF(std::string_view nameOrPath)` — cached by canonical absolute path
|
||||
- `std::optional<std::shared_ptr<LoadedGLTF>> loadGLTF(std::string_view nameOrPath, const GLTFLoadCallbacks *cb)` — with custom callbacks
|
||||
- `size_t prefetchGLTFTextures(std::string_view nameOrPath)` — schedule texture loads ahead of time
|
||||
- `GLTFTexturePrefetchResult prefetchGLTFTexturesWithHandles(std::string_view nameOrPath)` — returns handles for tracking
|
||||
- Meshes
|
||||
- `std::shared_ptr<MeshAsset> createMesh(const MeshCreateInfo &info)`
|
||||
- `std::shared_ptr<MeshAsset> createMesh(const std::string &name, std::span<Vertex> v, std::span<uint32_t> i, std::shared_ptr<GLTFMaterial> material = {})`
|
||||
- `std::shared_ptr<MeshAsset> createMesh(const std::string &name, std::span<Vertex> v, std::span<uint32_t> i, std::shared_ptr<GLTFMaterial> material = {}, bool build_bvh = true)`
|
||||
- `std::shared_ptr<MeshAsset> getMesh(const std::string &name) const`
|
||||
- `std::shared_ptr<MeshAsset> getPrimitive(std::string_view name) const` (returns existing default primitives if created)
|
||||
- `std::shared_ptr<MeshAsset> getPrimitive(std::string_view name) const` — returns existing default primitives if created
|
||||
- `bool removeMesh(const std::string &name)`
|
||||
- `bool removeMeshDeferred(const std::string &name, DeletionQueue &dq)` — deferred cleanup via deletion queue
|
||||
- `void cleanup()` — releases meshes, material buffers, and any images owned by the manager
|
||||
- Materials
|
||||
- `std::shared_ptr<GLTFMaterial> createMaterialFromConstants(const std::string &name, const GLTFMetallic_Roughness::MaterialConstants &constants, MaterialPass pass = MaterialPass::MainColor)` — create PBR material from constants using engine default textures
|
||||
|
||||
### Mesh Creation Model
|
||||
|
||||
@@ -41,15 +48,19 @@ struct AssetManager::MaterialOptions {
|
||||
std::string albedoPath; // resolved through AssetManager
|
||||
std::string metalRoughPath; // resolved through AssetManager
|
||||
std::string normalPath; // resolved through AssetManager (tangent-space normal)
|
||||
std::string occlusionPath; // resolved through AssetManager (ambient occlusion)
|
||||
std::string emissivePath; // resolved through AssetManager (emissive/glow)
|
||||
bool albedoSRGB = true; // VK_FORMAT_R8G8B8A8_SRGB when true
|
||||
bool metalRoughSRGB = false; // VK_FORMAT_R8G8B8A8_UNORM when false
|
||||
bool normalSRGB = false; // normal maps should be UNORM
|
||||
bool occlusionSRGB = false; // occlusion should be UNORM
|
||||
bool emissiveSRGB = true; // emissive is typically sRGB
|
||||
GLTFMetallic_Roughness::MaterialConstants constants{}; // extra[0].x as normalScale
|
||||
MaterialPass pass = MaterialPass::MainColor; // or Transparent
|
||||
};
|
||||
|
||||
struct AssetManager::MeshGeometryDesc {
|
||||
enum class Type { Provided, Cube, Sphere };
|
||||
enum class Type { Provided, Cube, Sphere, Plane, Capsule };
|
||||
Type type = Type::Provided;
|
||||
std::span<Vertex> vertices{}; // when Provided
|
||||
std::span<uint32_t> indices{}; // when Provided
|
||||
@@ -65,8 +76,9 @@ struct AssetManager::MeshMaterialDesc {
|
||||
|
||||
struct AssetManager::MeshCreateInfo {
|
||||
std::string name; // cache key; reused if already created
|
||||
MeshGeometryDesc geometry; // Provided / Cube / Sphere
|
||||
MeshGeometryDesc geometry; // Provided / Cube / Sphere / Plane / Capsule
|
||||
MeshMaterialDesc material; // Default or Textured
|
||||
std::optional<BoundsType> boundsType; // optional override for collision/picking bounds
|
||||
};
|
||||
```
|
||||
|
||||
@@ -113,9 +125,23 @@ si.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
|
||||
auto sphere = ctx->getAssets()->createMesh(si);
|
||||
ctx->scene->addMeshInstance("sphere.instance", sphere,
|
||||
glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f)));
|
||||
|
||||
// Plane primitive
|
||||
AssetManager::MeshCreateInfo pi{};
|
||||
pi.name = "groundPlane";
|
||||
pi.geometry.type = AssetManager::MeshGeometryDesc::Type::Plane;
|
||||
pi.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
|
||||
auto plane = ctx->getAssets()->createMesh(pi);
|
||||
|
||||
// Capsule primitive
|
||||
AssetManager::MeshCreateInfo capi{};
|
||||
capi.name = "capsuleA";
|
||||
capi.geometry.type = AssetManager::MeshGeometryDesc::Type::Capsule;
|
||||
capi.material.kind = AssetManager::MeshMaterialDesc::Kind::Default;
|
||||
auto capsule = ctx->getAssets()->createMesh(capi);
|
||||
```
|
||||
|
||||
Textured primitive (albedo + metal-rough + normal):
|
||||
Textured primitive (albedo + metal-rough + normal + occlusion + emissive):
|
||||
|
||||
```c++
|
||||
AssetManager::MeshCreateInfo ti{};
|
||||
@@ -128,6 +154,8 @@ ti.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured;
|
||||
ti.material.options.albedoPath = "textures/ground_albedo.png"; // sRGB
|
||||
ti.material.options.metalRoughPath = "textures/ground_mr.png"; // UNORM, G=roughness, B=metallic
|
||||
ti.material.options.normalPath = "textures/ground_n.png"; // UNORM
|
||||
ti.material.options.occlusionPath = "textures/ground_ao.png"; // UNORM (optional)
|
||||
ti.material.options.emissivePath = "textures/ground_emit.png"; // sRGB (optional)
|
||||
ti.material.options.constants.extra[0].x = 1.0f; // normalScale
|
||||
// ti.material.options.pass = MaterialPass::Transparent; // optional
|
||||
|
||||
@@ -136,7 +164,25 @@ glm::mat4 tx = glm::scale(glm::mat4(1.f), glm::vec3(10.f, 1.f, 10.f));
|
||||
ctx->scene->addMeshInstance("ground.textured", texturedPlane, tx);
|
||||
```
|
||||
|
||||
Textured cube/sphere via options is analogous — set `geometry.type` to `Cube` or `Sphere` and fill `material.options`.
|
||||
Textured cube/sphere/plane/capsule via options is analogous — set `geometry.type` to `Cube`, `Sphere`, `Plane`, or `Capsule` and fill `material.options`.
|
||||
|
||||
Using custom material from constants:
|
||||
|
||||
```c++
|
||||
// Create a material with custom PBR values (using engine default textures)
|
||||
GLTFMetallic_Roughness::MaterialConstants constants{};
|
||||
constants.colorFactors = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); // red
|
||||
constants.metal_rough_factors = glm::vec4(0.0f, 0.8f, 0.0f, 0.0f); // non-metallic, rough
|
||||
|
||||
auto redMaterial = ctx->getAssets()->createMaterialFromConstants(
|
||||
"red_rough_material",
|
||||
constants,
|
||||
MaterialPass::MainColor
|
||||
);
|
||||
|
||||
// Use with custom mesh
|
||||
auto mesh = ctx->getAssets()->createMesh("custom_mesh", vertices, indices, redMaterial);
|
||||
```
|
||||
|
||||
Runtime glTF spawning:
|
||||
|
||||
@@ -154,11 +200,74 @@ ctx->scene->addGLTFInstance("chair01", *chair,
|
||||
ctx->scene->removeGLTFInstance("chair01");
|
||||
```
|
||||
|
||||
### Texture Prefetching
|
||||
|
||||
Queue texture loads for a glTF file ahead of time. This parses the glTF, builds TextureCache keys for referenced images (both external URIs and embedded images in buffers), and issues `TextureCache::request()` calls. Actual uploads happen via the normal per-frame pump.
|
||||
|
||||
```c++
|
||||
// Simple version: returns number of textures scheduled
|
||||
size_t count = ctx->getAssets()->prefetchGLTFTextures("models/heavy_asset.glb");
|
||||
|
||||
// Advanced version: returns handles for tracking progress
|
||||
auto result = ctx->getAssets()->prefetchGLTFTexturesWithHandles("models/heavy_asset.glb");
|
||||
fmt::println("Scheduled {} textures", result.scheduled);
|
||||
// Use result.handles with TextureCache to monitor loading state
|
||||
```
|
||||
|
||||
Texture prefetching is particularly useful when combined with `AsyncAssetLoader` for loading large models in the background.
|
||||
|
||||
### Async Asset Loading
|
||||
|
||||
The `AsyncAssetLoader` class provides asynchronous glTF loading with worker threads for CPU-bound tasks (file I/O, parsing, mesh/BVH building). GPU uploads are still deferred through ResourceManager and the Render Graph.
|
||||
|
||||
```c++
|
||||
// Access via EngineContext
|
||||
auto *loader = ctx->async_loader;
|
||||
|
||||
// Queue a model to load in the background
|
||||
auto jobID = loader->load_gltf_async(
|
||||
"spaceship_01", // scene instance name
|
||||
"models/spaceship.glb", // model path (resolved via AssetManager)
|
||||
glm::translate(glm::mat4(1.f), glm::vec3(0, 5, -10)), // transform
|
||||
true // preload textures
|
||||
);
|
||||
|
||||
// Check progress in your update loop
|
||||
JobState state;
|
||||
float progress;
|
||||
std::string error;
|
||||
if (loader->get_job_status(jobID, state, progress, &error)) {
|
||||
if (state == JobState::Completed) {
|
||||
fmt::println("Model loaded successfully!");
|
||||
} else if (state == JobState::Failed) {
|
||||
fmt::println("Failed to load: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Commit completed jobs to the scene (call once per frame)
|
||||
loader->pump_main_thread(*ctx->scene);
|
||||
|
||||
// Alternative: use WorldVec3 for large-world coordinates
|
||||
auto jobID2 = loader->load_gltf_async(
|
||||
"distant_building",
|
||||
"models/building.glb",
|
||||
WorldVec3{1000000.0, 0.0, 500000.0}, // world position
|
||||
glm::quat(1.0f, 0.0f, 0.0f, 0.0f), // rotation
|
||||
glm::vec3(1.0f), // scale
|
||||
false // don't preload textures
|
||||
);
|
||||
```
|
||||
|
||||
The `AsyncAssetLoader` integrates with `TextureCache` to track texture streaming progress. When `preload_textures` is true, the loader will schedule all model textures for loading and track their residency state.
|
||||
|
||||
### Notes
|
||||
|
||||
- Default primitives: The engine creates default Cube/Sphere meshes via `AssetManager` and registers them as dynamic scene instances.
|
||||
- Default primitives: The engine creates default Cube/Sphere/Plane/Capsule meshes via `AssetManager` and registers them as dynamic scene instances.
|
||||
- Reuse by name: `createMesh("name", ...)` returns the cached mesh if it already exists. Use a unique name or call `removeMesh(name)` to replace.
|
||||
- sRGB/UNORM: Albedo is sRGB by default, metal-rough is UNORM by default. Adjust via `MaterialOptions`.
|
||||
- sRGB/UNORM: Albedo and emissive are sRGB by default, metal-rough/normal/occlusion are UNORM by default. Adjust via `MaterialOptions`.
|
||||
- Hot reload: Shaders are resolved via `shaderPath()`; pipeline hot reload is handled by the pipeline manager, not the AssetManager.
|
||||
- Normal maps: Supported. If `normalPath` is empty, a flat normal is used.
|
||||
- Occlusion & Emissive: Supported via `occlusionPath` and `emissivePath` in `MaterialOptions`.
|
||||
- Tangents: Loaded from glTF when present; otherwise generated. Enable MikkTSpace at configure time with `-DENABLE_MIKKTS=ON`.
|
||||
- BVH building: Enabled by default for meshes (`build_bvh = true`). Required for picking and ray-tracing.
|
||||
- Deferred cleanup: Use `removeMeshDeferred()` when destroying meshes during rendering to avoid destroying resources that are in-flight on the GPU.
|
||||
|
||||
@@ -35,6 +35,26 @@ const float SHADOW_MIN_BIAS = 1e-5;
|
||||
const float SHADOW_RAY_TMIN = 0.02;// start a bit away from the surface
|
||||
const float SHADOW_RAY_ORIGIN_BIAS = 0.01;// world units
|
||||
|
||||
// Estimate the float ULP scale at this world position magnitude, used to keep
|
||||
// ray bias and tMin effective even when world coordinates are very large.
|
||||
float world_pos_ulp(vec3 p)
|
||||
{
|
||||
float m = max(max(abs(p.x), abs(p.y)), abs(p.z));
|
||||
// For IEEE-754 float, relative precision is ~2^-23 (~1.192e-7). Clamp to a
|
||||
// small baseline to avoid tiny values near the origin.
|
||||
return max(1e-4, m * 1.1920929e-7);
|
||||
}
|
||||
|
||||
float shadow_ray_origin_bias(vec3 p)
|
||||
{
|
||||
return max(SHADOW_RAY_ORIGIN_BIAS, world_pos_ulp(p) * 8.0);
|
||||
}
|
||||
|
||||
float shadow_ray_tmin(vec3 p)
|
||||
{
|
||||
return max(SHADOW_RAY_TMIN, world_pos_ulp(p) * 16.0);
|
||||
}
|
||||
|
||||
vec3 getCameraWorldPosition()
|
||||
{
|
||||
// view = [ R^T -R^T*C ]
|
||||
@@ -223,9 +243,11 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
#ifdef GL_EXT_ray_query
|
||||
float farR = max(max(sceneData.cascadeSplitsView.x, sceneData.cascadeSplitsView.y),
|
||||
max(sceneData.cascadeSplitsView.z, sceneData.cascadeSplitsView.w));
|
||||
float originBias = shadow_ray_origin_bias(wp);
|
||||
float tmin = shadow_ray_tmin(wp);
|
||||
rayQueryEXT rq;
|
||||
rayQueryInitializeEXT(rq, topLevelAS, gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT,
|
||||
0xFF, wp + N * SHADOW_RAY_ORIGIN_BIAS, SHADOW_RAY_TMIN, L, farR);
|
||||
0xFF, wp + N * originBias, tmin, L, farR);
|
||||
while (rayQueryProceedEXT(rq)) { }
|
||||
bool hit = (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT);
|
||||
return hit ? 0.0 : 1.0;
|
||||
@@ -249,10 +271,12 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
if (cascadeEnabled && NoL < sceneData.rtParams.x)
|
||||
{
|
||||
float maxT = sceneData.cascadeSplitsView[cm.i0];
|
||||
float originBias = shadow_ray_origin_bias(wp);
|
||||
float tmin = shadow_ray_tmin(wp);
|
||||
rayQueryEXT rq;
|
||||
// tmin: small offset to avoid self-hits
|
||||
rayQueryInitializeEXT(rq, topLevelAS, gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT,
|
||||
0xFF, wp + N * SHADOW_RAY_ORIGIN_BIAS, SHADOW_RAY_TMIN, L, maxT);
|
||||
0xFF, wp + N * originBias, tmin, L, maxT);
|
||||
bool hit = false;
|
||||
while (rayQueryProceedEXT(rq)) { }
|
||||
hit = (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT);
|
||||
@@ -278,9 +302,11 @@ float calcShadowVisibility(vec3 worldPos, vec3 N, vec3 L)
|
||||
float maxT0 = sceneData.cascadeSplitsView[cm.i0];
|
||||
float maxT1 = sceneData.cascadeSplitsView[cm.i1];
|
||||
float maxT = max(maxT0, maxT1);
|
||||
float originBias = shadow_ray_origin_bias(wp);
|
||||
float tmin = shadow_ray_tmin(wp);
|
||||
rayQueryEXT rq;
|
||||
rayQueryInitializeEXT(rq, topLevelAS, gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT,
|
||||
0xFF, wp + N * SHADOW_RAY_ORIGIN_BIAS, SHADOW_RAY_TMIN, L, maxT);
|
||||
0xFF, wp + N * originBias, tmin, L, maxT);
|
||||
while (rayQueryProceedEXT(rq)) { }
|
||||
bool hit = (rayQueryGetIntersectionTypeEXT(rq, true) != gl_RayQueryCommittedIntersectionNoneEXT);
|
||||
if (hit) vis = min(vis, 0.0);
|
||||
@@ -335,7 +361,9 @@ void main(){
|
||||
if (maxT > 0.01)
|
||||
{
|
||||
vec3 dir = toL / maxT;
|
||||
vec3 origin = pos + N * SHADOW_RAY_ORIGIN_BIAS;
|
||||
float originBias = shadow_ray_origin_bias(pos);
|
||||
float tmin = shadow_ray_tmin(pos);
|
||||
vec3 origin = pos + N * originBias;
|
||||
|
||||
rayQueryEXT rq;
|
||||
rayQueryInitializeEXT(
|
||||
@@ -344,7 +372,7 @@ void main(){
|
||||
gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT,
|
||||
0xFF,
|
||||
origin,
|
||||
SHADOW_RAY_TMIN,
|
||||
tmin,
|
||||
dir,
|
||||
maxT
|
||||
);
|
||||
@@ -380,7 +408,9 @@ void main(){
|
||||
float cosTheta = dot(-L, dir);
|
||||
if (cosTheta > sceneData.spotLights[i].direction_cos_outer.w)
|
||||
{
|
||||
vec3 origin = pos + N * SHADOW_RAY_ORIGIN_BIAS;
|
||||
float originBias = shadow_ray_origin_bias(pos);
|
||||
float tmin = shadow_ray_tmin(pos);
|
||||
vec3 origin = pos + N * originBias;
|
||||
|
||||
rayQueryEXT rq;
|
||||
rayQueryInitializeEXT(
|
||||
@@ -389,7 +419,7 @@ void main(){
|
||||
gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT,
|
||||
0xFF,
|
||||
origin,
|
||||
SHADOW_RAY_TMIN,
|
||||
tmin,
|
||||
L,
|
||||
maxT
|
||||
);
|
||||
|
||||
@@ -117,6 +117,25 @@ add_executable (vulkan_engine
|
||||
scene/tangent_space.cpp
|
||||
scene/camera.h
|
||||
scene/camera.cpp
|
||||
scene/camera/icamera_mode.h
|
||||
scene/camera/camera_rig.h
|
||||
scene/camera/camera_rig.cpp
|
||||
scene/camera/mode_free.h
|
||||
scene/camera/mode_free.cpp
|
||||
scene/camera/mode_orbit.h
|
||||
scene/camera/mode_orbit.cpp
|
||||
scene/camera/mode_follow.h
|
||||
scene/camera/mode_follow.cpp
|
||||
scene/camera/mode_chase.h
|
||||
scene/camera/mode_chase.cpp
|
||||
scene/camera/mode_fixed.h
|
||||
scene/camera/mode_fixed.cpp
|
||||
scene/planet/cubesphere.h
|
||||
scene/planet/cubesphere.cpp
|
||||
scene/planet/planet_quadtree.h
|
||||
scene/planet/planet_quadtree.cpp
|
||||
scene/planet/planet_system.h
|
||||
scene/planet/planet_system.cpp
|
||||
# compute
|
||||
compute/vk_compute.h
|
||||
compute/vk_compute.cpp
|
||||
@@ -182,7 +201,7 @@ if (CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDeb
|
||||
/OPT:ICF
|
||||
)
|
||||
# Optional: AVX2
|
||||
# target_compile_options(vulkan_engine PRIVATE /arch:AVX2)
|
||||
target_compile_options(vulkan_engine PRIVATE /arch:AVX2)
|
||||
|
||||
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
# GCC (g++)
|
||||
@@ -298,23 +317,73 @@ target_include_directories(vma_impl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thir
|
||||
target_link_libraries(vma_impl PRIVATE Vulkan::Vulkan)
|
||||
target_link_libraries(vulkan_engine PUBLIC $<TARGET_OBJECTS:vma_impl>)
|
||||
|
||||
target_precompile_headers(vulkan_engine PUBLIC <optional> <vector> <memory> <string> <vector> <unordered_map> <glm/mat4x4.hpp> <glm/vec4.hpp> <vulkan/vulkan.h>)
|
||||
target_precompile_headers(vulkan_engine PRIVATE
|
||||
<optional>
|
||||
<vector>
|
||||
<memory>
|
||||
<string>
|
||||
<unordered_map>
|
||||
<glm/mat4x4.hpp>
|
||||
<glm/vec4.hpp>
|
||||
<vulkan/vulkan.h>
|
||||
)
|
||||
|
||||
add_custom_command(TARGET vulkan_engine POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:vulkan_engine> $<TARGET_FILE_DIR:vulkan_engine>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
if(WIN32)
|
||||
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.21)
|
||||
add_custom_command(TARGET vulkan_engine POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_RUNTIME_DLLS:vulkan_engine> $<TARGET_FILE_DIR:vulkan_engine>
|
||||
COMMAND_EXPAND_LISTS
|
||||
)
|
||||
else()
|
||||
message(WARNING "CMake < 3.21: skipping runtime DLL copy step for vulkan_engine")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
find_package(ktx CONFIG REQUIRED)
|
||||
set(_KTX_TARGET "")
|
||||
if (TARGET ktx::ktx)
|
||||
set(_KTX_TARGET ktx::ktx)
|
||||
elseif (TARGET KTX::ktx)
|
||||
set(_KTX_TARGET KTX::ktx)
|
||||
elseif (TARGET ktx)
|
||||
set(_KTX_TARGET ktx)
|
||||
# KTX (libktx)
|
||||
# NOTE: Under WSL it's easy to accidentally pick up the Windows KTX package (ktx.dll),
|
||||
# which will configure but fail to link on Linux. Prefer the native library when needed.
|
||||
find_package(ktx CONFIG QUIET)
|
||||
|
||||
set(_KTX_CMAKE_TARGET "")
|
||||
if(TARGET ktx::ktx)
|
||||
set(_KTX_CMAKE_TARGET ktx::ktx)
|
||||
elseif(TARGET KTX::ktx)
|
||||
set(_KTX_CMAKE_TARGET KTX::ktx)
|
||||
elseif(TARGET ktx)
|
||||
set(_KTX_CMAKE_TARGET ktx)
|
||||
endif()
|
||||
if (_KTX_TARGET STREQUAL "")
|
||||
message(FATAL_ERROR "libktx not found; please install KTX v2 and expose its CMake package")
|
||||
|
||||
set(_KTX_USE_FALLBACK OFF)
|
||||
if(_KTX_CMAKE_TARGET STREQUAL "")
|
||||
set(_KTX_USE_FALLBACK ON)
|
||||
else()
|
||||
get_target_property(_KTX_IMPORTED "${_KTX_CMAKE_TARGET}" IMPORTED)
|
||||
if(_KTX_IMPORTED)
|
||||
set(_KTX_IMPORTED_LOCATION "")
|
||||
get_target_property(_KTX_IMPORTED_LOCATION "${_KTX_CMAKE_TARGET}" IMPORTED_LOCATION)
|
||||
if(NOT _KTX_IMPORTED_LOCATION)
|
||||
get_target_property(_KTX_IMPORTED_LOCATION "${_KTX_CMAKE_TARGET}" IMPORTED_LOCATION_RELEASE)
|
||||
endif()
|
||||
if(NOT _KTX_IMPORTED_LOCATION)
|
||||
get_target_property(_KTX_IMPORTED_LOCATION "${_KTX_CMAKE_TARGET}" IMPORTED_LOCATION_DEBUG)
|
||||
endif()
|
||||
|
||||
if(NOT _KTX_IMPORTED_LOCATION)
|
||||
set(_KTX_USE_FALLBACK ON)
|
||||
elseif(NOT WIN32 AND _KTX_IMPORTED_LOCATION MATCHES "\\.dll$|\\.lib$")
|
||||
set(_KTX_USE_FALLBACK ON)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(_KTX_USE_FALLBACK)
|
||||
find_path(KTX_INCLUDE_DIR NAMES ktx.h)
|
||||
find_library(KTX_LIBRARY NAMES ktx)
|
||||
if(NOT KTX_INCLUDE_DIR OR NOT KTX_LIBRARY)
|
||||
message(FATAL_ERROR "libktx not found; please install KTX v2 and make it discoverable via ktx_DIR or CMAKE_PREFIX_PATH")
|
||||
endif()
|
||||
target_include_directories(vulkan_engine PUBLIC "${KTX_INCLUDE_DIR}")
|
||||
target_link_libraries(vulkan_engine PUBLIC "${KTX_LIBRARY}")
|
||||
else()
|
||||
target_link_libraries(vulkan_engine PUBLIC "${_KTX_CMAKE_TARGET}")
|
||||
endif()
|
||||
target_link_libraries(vulkan_engine PUBLIC ${_KTX_TARGET})
|
||||
|
||||
@@ -587,7 +587,8 @@ std::pair<AllocatedImage, bool> AssetManager::loadImageFromAsset(std::string_vie
|
||||
std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
|
||||
std::span<Vertex> vertices,
|
||||
std::span<uint32_t> indices,
|
||||
std::shared_ptr<GLTFMaterial> material)
|
||||
std::shared_ptr<GLTFMaterial> material,
|
||||
bool build_bvh)
|
||||
{
|
||||
if (!_engine || !_engine->_resourceManager) return {};
|
||||
if (name.empty()) return {};
|
||||
@@ -631,9 +632,12 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
|
||||
surf.bounds = compute_bounds(vertices);
|
||||
mesh->surfaces.push_back(surf);
|
||||
|
||||
// Build CPU-side BVH for precise ray picking over this mesh.
|
||||
// This uses the same mesh-local vertex/index data as the GPU upload.
|
||||
mesh->bvh = build_mesh_bvh(*mesh, vertices, indices);
|
||||
if (build_bvh)
|
||||
{
|
||||
// Build CPU-side BVH for precise ray picking over this mesh.
|
||||
// This uses the same mesh-local vertex/index data as the GPU upload.
|
||||
mesh->bvh = build_mesh_bvh(*mesh, vertices, indices);
|
||||
}
|
||||
|
||||
_meshCache.emplace(name, mesh);
|
||||
return mesh;
|
||||
@@ -665,6 +669,26 @@ std::shared_ptr<GLTFMaterial> AssetManager::createMaterialFromConstants(
|
||||
return createMaterial(pass, res);
|
||||
}
|
||||
|
||||
VkImageView AssetManager::fallback_checkerboard_view() const
|
||||
{
|
||||
return (_engine) ? _engine->_errorCheckerboardImage.imageView : VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
VkImageView AssetManager::fallback_white_view() const
|
||||
{
|
||||
return (_engine) ? _engine->_whiteImage.imageView : VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
VkImageView AssetManager::fallback_flat_normal_view() const
|
||||
{
|
||||
return (_engine) ? _engine->_flatNormalImage.imageView : VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
VkImageView AssetManager::fallback_black_view() const
|
||||
{
|
||||
return (_engine) ? _engine->_blackImage.imageView : VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
std::shared_ptr<MeshAsset> AssetManager::getMesh(const std::string &name) const
|
||||
{
|
||||
auto it = _meshCache.find(name);
|
||||
@@ -709,3 +733,65 @@ bool AssetManager::removeMesh(const std::string &name)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetManager::removeMeshDeferred(const std::string &name, DeletionQueue &dq)
|
||||
{
|
||||
auto it = _meshCache.find(name);
|
||||
if (it == _meshCache.end()) return false;
|
||||
|
||||
const std::shared_ptr<MeshAsset> mesh = it->second;
|
||||
if (!mesh) return false;
|
||||
|
||||
// Remove from cache immediately so callers won't retrieve a mesh we plan to destroy.
|
||||
_meshCache.erase(it);
|
||||
|
||||
if (_engine && _engine->_rayManager)
|
||||
{
|
||||
// Clean up BLAS cached for this mesh (if ray tracing is enabled).
|
||||
// RayTracingManager defers actual AS destruction internally.
|
||||
_engine->_rayManager->removeBLASForBuffer(mesh->meshBuffers.vertexBuffer.buffer);
|
||||
}
|
||||
|
||||
ResourceManager *rm = (_engine && _engine->_resourceManager) ? _engine->_resourceManager.get() : nullptr;
|
||||
if (!rm)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const AllocatedBuffer indexBuffer = mesh->meshBuffers.indexBuffer;
|
||||
const AllocatedBuffer vertexBuffer = mesh->meshBuffers.vertexBuffer;
|
||||
|
||||
std::optional<AllocatedBuffer> materialBuffer;
|
||||
auto itb = _meshMaterialBuffers.find(name);
|
||||
if (itb != _meshMaterialBuffers.end())
|
||||
{
|
||||
materialBuffer = itb->second;
|
||||
_meshMaterialBuffers.erase(itb);
|
||||
}
|
||||
|
||||
std::vector<AllocatedImage> ownedImages;
|
||||
auto iti = _meshOwnedImages.find(name);
|
||||
if (iti != _meshOwnedImages.end())
|
||||
{
|
||||
ownedImages = std::move(iti->second);
|
||||
_meshOwnedImages.erase(iti);
|
||||
}
|
||||
|
||||
dq.push_function([rm, indexBuffer, vertexBuffer, materialBuffer, ownedImages = std::move(ownedImages)]() mutable
|
||||
{
|
||||
if (indexBuffer.buffer) rm->destroy_buffer(indexBuffer);
|
||||
if (vertexBuffer.buffer) rm->destroy_buffer(vertexBuffer);
|
||||
|
||||
if (materialBuffer.has_value() && materialBuffer->buffer)
|
||||
{
|
||||
rm->destroy_buffer(*materialBuffer);
|
||||
}
|
||||
|
||||
for (const auto &img : ownedImages)
|
||||
{
|
||||
if (img.image) rm->destroy_image(img);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -107,17 +107,25 @@ public:
|
||||
std::shared_ptr<MeshAsset> createMesh(const std::string &name,
|
||||
std::span<Vertex> vertices,
|
||||
std::span<uint32_t> indices,
|
||||
std::shared_ptr<GLTFMaterial> material = {});
|
||||
std::shared_ptr<GLTFMaterial> material = {},
|
||||
bool build_bvh = true);
|
||||
|
||||
std::shared_ptr<MeshAsset> getMesh(const std::string &name) const;
|
||||
|
||||
bool removeMesh(const std::string &name);
|
||||
bool removeMeshDeferred(const std::string &name, DeletionQueue &dq);
|
||||
|
||||
// Convenience: create a PBR material from constants using engine default textures
|
||||
std::shared_ptr<GLTFMaterial> createMaterialFromConstants(const std::string &name,
|
||||
const GLTFMetallic_Roughness::MaterialConstants &constants,
|
||||
MaterialPass pass = MaterialPass::MainColor);
|
||||
|
||||
// Access engine-provided fallback textures for procedural systems.
|
||||
VkImageView fallback_checkerboard_view() const;
|
||||
VkImageView fallback_white_view() const;
|
||||
VkImageView fallback_flat_normal_view() const;
|
||||
VkImageView fallback_black_view() const;
|
||||
|
||||
const AssetPaths &paths() const { return _locator.paths(); }
|
||||
void setPaths(const AssetPaths &p) { _locator.setPaths(p); }
|
||||
|
||||
|
||||
@@ -359,6 +359,44 @@ GPUMeshBuffers ResourceManager::uploadMesh(std::span<uint32_t> indices, std::spa
|
||||
return newSurface;
|
||||
}
|
||||
|
||||
AllocatedBuffer ResourceManager::upload_buffer(const void *data, size_t size, VkBufferUsageFlags usage,
|
||||
VmaMemoryUsage memoryUsage)
|
||||
{
|
||||
if (data == nullptr || size == 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
AllocatedBuffer dst = create_buffer(size, usage | VK_BUFFER_USAGE_TRANSFER_DST_BIT, memoryUsage);
|
||||
|
||||
AllocatedBuffer staging = create_buffer(size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
|
||||
VMA_MEMORY_USAGE_CPU_ONLY);
|
||||
|
||||
memcpy(staging.info.pMappedData, data, size);
|
||||
vmaFlushAllocation(_deviceManager->allocator(), staging.allocation, 0, size);
|
||||
|
||||
PendingBufferUpload pending{};
|
||||
pending.staging = staging;
|
||||
pending.copies.push_back(BufferCopyRegion{
|
||||
.destination = dst.buffer,
|
||||
.dstOffset = 0,
|
||||
.size = size,
|
||||
.stagingOffset = 0,
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(_pendingMutex);
|
||||
_pendingBufferUploads.push_back(std::move(pending));
|
||||
}
|
||||
|
||||
if (!_deferUploads)
|
||||
{
|
||||
process_queued_uploads_immediate();
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
bool ResourceManager::has_pending_uploads() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(_pendingMutex);
|
||||
|
||||
@@ -94,6 +94,12 @@ public:
|
||||
|
||||
GPUMeshBuffers uploadMesh(std::span<uint32_t> indices, std::span<Vertex> vertices);
|
||||
|
||||
// Upload raw bytes into a GPU buffer. The destination buffer is created with the provided 'usage'
|
||||
// flags plus VK_BUFFER_USAGE_TRANSFER_DST_BIT. Staging is handled internally and freed via the
|
||||
// per-frame deletion queue when deferred uploads are enabled.
|
||||
AllocatedBuffer upload_buffer(const void *data, size_t size, VkBufferUsageFlags usage,
|
||||
VmaMemoryUsage memoryUsage = VMA_MEMORY_USAGE_GPU_ONLY);
|
||||
|
||||
void immediate_submit(std::function<void(VkCommandBuffer)> &&function) const;
|
||||
|
||||
bool has_pending_uploads() const;
|
||||
|
||||
@@ -1671,7 +1671,7 @@ void VulkanEngine::run()
|
||||
|
||||
if (_sceneManager && _input)
|
||||
{
|
||||
_sceneManager->getMainCamera().process_input(*_input, ui_capture_keyboard, ui_capture_mouse);
|
||||
_sceneManager->getCameraRig().process_input(*_input, ui_capture_keyboard, ui_capture_mouse);
|
||||
}
|
||||
|
||||
if (freeze_rendering)
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
#include "core/assets/ibl_manager.h"
|
||||
#include "core/ui/imgui_system.h"
|
||||
#include "core/picking/picking_system.h"
|
||||
#include "core/input/input_system.h"
|
||||
|
||||
class InputSystem;
|
||||
class DebugDrawSystem;
|
||||
|
||||
struct DebugDrawDeleter
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "render/passes/background.h"
|
||||
#include "render/passes/particles.h"
|
||||
#include <glm/gtx/euler_angles.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include "render/graph/graph.h"
|
||||
#include "core/pipeline/manager.h"
|
||||
@@ -36,6 +37,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "mesh_bvh.h"
|
||||
#include "scene/planet/planet_system.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -781,6 +783,213 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_camera(VulkanEngine *eng)
|
||||
{
|
||||
if (!eng || !eng->_sceneManager)
|
||||
{
|
||||
ImGui::TextUnformatted("SceneManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager *sceneMgr = eng->_sceneManager.get();
|
||||
CameraRig &rig = sceneMgr->getCameraRig();
|
||||
Camera &cam = sceneMgr->getMainCamera();
|
||||
|
||||
// Mode switch
|
||||
static const char *k_mode_names[] = {"Free", "Orbit", "Follow", "Chase", "Fixed"};
|
||||
int mode = static_cast<int>(rig.mode());
|
||||
if (ImGui::Combo("Mode", &mode, k_mode_names, IM_ARRAYSIZE(k_mode_names)))
|
||||
{
|
||||
rig.set_mode(static_cast<CameraMode>(mode), *sceneMgr, cam);
|
||||
if (eng->_input)
|
||||
{
|
||||
eng->_input->set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Text("Active mode: %s", rig.mode_name());
|
||||
ImGui::Separator();
|
||||
|
||||
// Camera state (world)
|
||||
double pos[3] = {cam.position_world.x, cam.position_world.y, cam.position_world.z};
|
||||
if (ImGui::InputScalarN("Position (world)", ImGuiDataType_Double, pos, 3, nullptr, nullptr, "%.3f"))
|
||||
{
|
||||
cam.position_world = WorldVec3(pos[0], pos[1], pos[2]);
|
||||
}
|
||||
float fov = cam.fovDegrees;
|
||||
if (ImGui::SliderFloat("FOV (deg)", &fov, 30.0f, 110.0f))
|
||||
{
|
||||
cam.fovDegrees = fov;
|
||||
}
|
||||
|
||||
WorldVec3 origin = sceneMgr->get_world_origin();
|
||||
glm::vec3 camLocal = sceneMgr->get_camera_local_position();
|
||||
ImGui::Text("Origin (world): (%.3f, %.3f, %.3f)", origin.x, origin.y, origin.z);
|
||||
ImGui::Text("Camera (local): (%.3f, %.3f, %.3f)", camLocal.x, camLocal.y, camLocal.z);
|
||||
|
||||
auto target_from_last_pick = [&](CameraTarget &target) -> bool {
|
||||
PickingSystem *picking = eng->picking();
|
||||
if (!picking) return false;
|
||||
const auto &pick = picking->last_pick();
|
||||
if (!pick.valid) return false;
|
||||
|
||||
if (pick.ownerType == RenderObject::OwnerType::MeshInstance)
|
||||
{
|
||||
target.type = CameraTargetType::MeshInstance;
|
||||
target.name = pick.ownerName;
|
||||
}
|
||||
else if (pick.ownerType == RenderObject::OwnerType::GLTFInstance)
|
||||
{
|
||||
target.type = CameraTargetType::GLTFInstance;
|
||||
target.name = pick.ownerName;
|
||||
}
|
||||
else
|
||||
{
|
||||
target.type = CameraTargetType::WorldPoint;
|
||||
target.world_point = pick.worldPos;
|
||||
target.name.clear();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto draw_target = [&](const char *id, CameraTarget &target, char *name_buf, size_t name_buf_size) {
|
||||
ImGui::PushID(id);
|
||||
static const char *k_target_types[] = {"None", "WorldPoint", "MeshInstance", "GLTFInstance"};
|
||||
int type = static_cast<int>(target.type);
|
||||
if (ImGui::Combo("Target type", &type, k_target_types, IM_ARRAYSIZE(k_target_types)))
|
||||
{
|
||||
target.type = static_cast<CameraTargetType>(type);
|
||||
if (target.type != CameraTargetType::MeshInstance && target.type != CameraTargetType::GLTFInstance)
|
||||
{
|
||||
target.name.clear();
|
||||
if (name_buf_size > 0)
|
||||
{
|
||||
name_buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (target.type == CameraTargetType::WorldPoint)
|
||||
{
|
||||
double p[3] = {target.world_point.x, target.world_point.y, target.world_point.z};
|
||||
if (ImGui::InputScalarN("World point", ImGuiDataType_Double, p, 3, nullptr, nullptr, "%.3f"))
|
||||
{
|
||||
target.world_point = WorldVec3(p[0], p[1], p[2]);
|
||||
}
|
||||
}
|
||||
else if (target.type == CameraTargetType::MeshInstance || target.type == CameraTargetType::GLTFInstance)
|
||||
{
|
||||
if (std::strncmp(name_buf, target.name.c_str(), name_buf_size) != 0)
|
||||
{
|
||||
std::snprintf(name_buf, name_buf_size, "%s", target.name.c_str());
|
||||
}
|
||||
ImGui::InputText("Target name", name_buf, name_buf_size);
|
||||
target.name = name_buf;
|
||||
}
|
||||
|
||||
WorldVec3 tpos{};
|
||||
glm::quat trot{};
|
||||
bool ok = rig.resolve_target(*sceneMgr, target, tpos, trot);
|
||||
ImGui::Text("Resolved: %s", ok ? "yes" : "no");
|
||||
if (ok)
|
||||
{
|
||||
ImGui::Text("Target world: (%.3f, %.3f, %.3f)", tpos.x, tpos.y, tpos.z);
|
||||
}
|
||||
ImGui::PopID();
|
||||
};
|
||||
|
||||
// Free
|
||||
if (ImGui::CollapsingHeader("Free", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
auto &s = rig.free_settings();
|
||||
ImGui::InputFloat("Move speed (u/s)", &s.move_speed);
|
||||
s.move_speed = std::clamp(s.move_speed, 0.06f, 300.0f);
|
||||
ImGui::InputFloat("Look sensitivity", &s.look_sensitivity);
|
||||
ImGui::InputFloat("Roll speed (rad/s)", &s.roll_speed);
|
||||
ImGui::TextUnformatted("Roll keys: Q/E");
|
||||
}
|
||||
|
||||
// Orbit
|
||||
if (ImGui::CollapsingHeader("Orbit"))
|
||||
{
|
||||
auto &s = rig.orbit_settings();
|
||||
static char orbitName[128] = "";
|
||||
draw_target("orbit_target", s.target, orbitName, IM_ARRAYSIZE(orbitName));
|
||||
if (ImGui::Button("Orbit target = Last Pick"))
|
||||
{
|
||||
target_from_last_pick(s.target);
|
||||
}
|
||||
ImGui::InputDouble("Distance", &s.distance, 0.1, 1.0, "%.3f");
|
||||
s.distance = std::clamp(s.distance, 0.2, 100000.0);
|
||||
float yawDeg = glm::degrees(s.yaw);
|
||||
float pitchDeg = glm::degrees(s.pitch);
|
||||
if (ImGui::SliderFloat("Yaw (deg)", &yawDeg, -180.0f, 180.0f))
|
||||
{
|
||||
s.yaw = glm::radians(yawDeg);
|
||||
}
|
||||
if (ImGui::SliderFloat("Pitch (deg)", &pitchDeg, -89.0f, 89.0f))
|
||||
{
|
||||
s.pitch = glm::radians(pitchDeg);
|
||||
}
|
||||
ImGui::InputFloat("Look sensitivity##orbit", &s.look_sensitivity);
|
||||
}
|
||||
|
||||
// Follow
|
||||
if (ImGui::CollapsingHeader("Follow"))
|
||||
{
|
||||
auto &s = rig.follow_settings();
|
||||
static char followName[128] = "";
|
||||
draw_target("follow_target", s.target, followName, IM_ARRAYSIZE(followName));
|
||||
if (ImGui::Button("Follow target = Last Pick"))
|
||||
{
|
||||
target_from_last_pick(s.target);
|
||||
}
|
||||
ImGui::InputFloat3("Position offset (local)", &s.position_offset_local.x);
|
||||
|
||||
glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(s.rotation_offset));
|
||||
float r[3] = {rotDeg.x, rotDeg.y, rotDeg.z};
|
||||
if (ImGui::InputFloat3("Rotation offset (deg XYZ)", r))
|
||||
{
|
||||
glm::mat4 R = glm::eulerAngleXYZ(glm::radians(r[0]), glm::radians(r[1]), glm::radians(r[2]));
|
||||
s.rotation_offset = glm::quat_cast(R);
|
||||
}
|
||||
if (ImGui::Button("Reset rotation offset"))
|
||||
{
|
||||
s.rotation_offset = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Chase
|
||||
if (ImGui::CollapsingHeader("Chase"))
|
||||
{
|
||||
auto &s = rig.chase_settings();
|
||||
static char chaseName[128] = "";
|
||||
draw_target("chase_target", s.target, chaseName, IM_ARRAYSIZE(chaseName));
|
||||
if (ImGui::Button("Chase target = Last Pick"))
|
||||
{
|
||||
target_from_last_pick(s.target);
|
||||
}
|
||||
ImGui::InputFloat3("Position offset (local)##chase", &s.position_offset_local.x);
|
||||
|
||||
glm::vec3 rotDeg = glm::degrees(glm::eulerAngles(s.rotation_offset));
|
||||
float r[3] = {rotDeg.x, rotDeg.y, rotDeg.z};
|
||||
if (ImGui::InputFloat3("Rotation offset (deg XYZ)##chase", r))
|
||||
{
|
||||
glm::mat4 R = glm::eulerAngleXYZ(glm::radians(r[0]), glm::radians(r[1]), glm::radians(r[2]));
|
||||
s.rotation_offset = glm::quat_cast(R);
|
||||
}
|
||||
|
||||
ImGui::SliderFloat("Position lag (1/s)", &s.position_lag, 0.0f, 30.0f);
|
||||
ImGui::SliderFloat("Rotation lag (1/s)", &s.rotation_lag, 0.0f, 30.0f);
|
||||
}
|
||||
|
||||
// Fixed
|
||||
if (ImGui::CollapsingHeader("Fixed"))
|
||||
{
|
||||
ImGui::TextUnformatted("Fixed mode does not modify the camera automatically.");
|
||||
}
|
||||
}
|
||||
|
||||
// Texture streaming + budget UI
|
||||
static const char *stateName(uint8_t s)
|
||||
{
|
||||
@@ -1386,97 +1595,17 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// Scene debug bits
|
||||
static void ui_scene(VulkanEngine *eng)
|
||||
// Scene editor - spawn and delete instances
|
||||
static void ui_scene_editor(VulkanEngine *eng)
|
||||
{
|
||||
if (!eng) return;
|
||||
const DrawContext &dc = eng->_context->getMainDrawContext();
|
||||
ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size());
|
||||
ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size());
|
||||
if (!eng || !eng->_sceneManager)
|
||||
{
|
||||
ImGui::TextUnformatted("SceneManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager *sceneMgr = eng->_sceneManager.get();
|
||||
PickingSystem *picking = eng->picking();
|
||||
if (picking)
|
||||
{
|
||||
bool use_id = picking->use_id_buffer_picking();
|
||||
if (ImGui::Checkbox("Use ID-buffer picking", &use_id))
|
||||
{
|
||||
picking->set_use_id_buffer_picking(use_id);
|
||||
}
|
||||
ImGui::Text("Picking mode: %s",
|
||||
use_id ? "ID buffer (async, 1-frame latency)" : "CPU raycast");
|
||||
|
||||
bool debug_bvh = picking->debug_draw_bvh();
|
||||
if (ImGui::Checkbox("Debug draw mesh BVH (last pick)", &debug_bvh))
|
||||
{
|
||||
picking->set_debug_draw_bvh(debug_bvh);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextUnformatted("Picking system not available");
|
||||
}
|
||||
|
||||
// Debug draw settings (engine-owned collector + render pass)
|
||||
if (eng->_context && eng->_context->debug_draw)
|
||||
{
|
||||
DebugDrawSystem *dd = eng->_context->debug_draw;
|
||||
auto &s = dd->settings();
|
||||
|
||||
bool enabled = s.enabled;
|
||||
if (ImGui::Checkbox("Enable debug draw", &enabled))
|
||||
{
|
||||
s.enabled = enabled;
|
||||
}
|
||||
if (s.enabled)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("Commands: %zu", dd->command_count());
|
||||
|
||||
int seg = s.segments;
|
||||
if (ImGui::SliderInt("Circle segments", &seg, 3, 128))
|
||||
{
|
||||
s.segments = seg;
|
||||
}
|
||||
|
||||
bool depth_tested = s.show_depth_tested;
|
||||
bool overlay = s.show_overlay;
|
||||
if (ImGui::Checkbox("Depth-tested", &depth_tested))
|
||||
{
|
||||
s.show_depth_tested = depth_tested;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Checkbox("Overlay", &overlay))
|
||||
{
|
||||
s.show_overlay = overlay;
|
||||
}
|
||||
|
||||
auto layer_checkbox = [&s](const char *label, DebugDrawLayer layer) {
|
||||
const uint32_t bit = static_cast<uint32_t>(layer);
|
||||
bool on = (s.layer_mask & bit) != 0u;
|
||||
if (ImGui::Checkbox(label, &on))
|
||||
{
|
||||
if (on) s.layer_mask |= bit;
|
||||
else s.layer_mask &= ~bit;
|
||||
}
|
||||
};
|
||||
|
||||
ImGui::TextUnformatted("Layers");
|
||||
layer_checkbox("Physics##dd_layer_physics", DebugDrawLayer::Physics);
|
||||
ImGui::SameLine();
|
||||
layer_checkbox("Picking##dd_layer_picking", DebugDrawLayer::Picking);
|
||||
ImGui::SameLine();
|
||||
layer_checkbox("Lights##dd_layer_lights", DebugDrawLayer::Lights);
|
||||
layer_checkbox("Particles##dd_layer_particles", DebugDrawLayer::Particles);
|
||||
ImGui::SameLine();
|
||||
layer_checkbox("Volumetrics##dd_layer_volumetrics", DebugDrawLayer::Volumetrics);
|
||||
ImGui::SameLine();
|
||||
layer_checkbox("Misc##dd_layer_misc", DebugDrawLayer::Misc);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextUnformatted("Debug draw system not available");
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
// Spawn glTF instances (runtime)
|
||||
ImGui::TextUnformatted("Spawn glTF instance");
|
||||
@@ -1530,13 +1659,73 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// Point light editor
|
||||
if (eng->_sceneManager)
|
||||
ImGui::Separator();
|
||||
// Delete selected model/primitive (uses last pick if valid, otherwise hover)
|
||||
static std::string deleteStatus;
|
||||
if (ImGui::Button("Delete selected"))
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Point lights");
|
||||
deleteStatus.clear();
|
||||
const PickingSystem::PickInfo *pick = nullptr;
|
||||
if (picking)
|
||||
{
|
||||
const auto &last = picking->last_pick();
|
||||
const auto &hover = picking->hover_pick();
|
||||
pick = last.valid ? &last : (hover.valid ? &hover : nullptr);
|
||||
}
|
||||
if (!pick || pick->ownerName.empty())
|
||||
{
|
||||
deleteStatus = "No selection to delete.";
|
||||
}
|
||||
else if (pick->ownerType == RenderObject::OwnerType::MeshInstance)
|
||||
{
|
||||
bool ok = eng->_sceneManager->removeMeshInstance(pick->ownerName);
|
||||
if (ok && picking)
|
||||
{
|
||||
picking->clear_owner_picks(RenderObject::OwnerType::MeshInstance, pick->ownerName);
|
||||
}
|
||||
deleteStatus = ok ? "Removed mesh instance: " + pick->ownerName
|
||||
: "Mesh instance not found: " + pick->ownerName;
|
||||
}
|
||||
else if (pick->ownerType == RenderObject::OwnerType::GLTFInstance)
|
||||
{
|
||||
bool ok = eng->_sceneManager->removeGLTFInstance(pick->ownerName);
|
||||
if (ok)
|
||||
{
|
||||
deleteStatus = "Removed glTF instance: " + pick->ownerName;
|
||||
if (picking)
|
||||
{
|
||||
picking->clear_owner_picks(RenderObject::OwnerType::GLTFInstance, pick->ownerName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deleteStatus = "glTF instance not found: " + pick->ownerName;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deleteStatus = "Cannot delete this object type (static scene).";
|
||||
}
|
||||
}
|
||||
if (!deleteStatus.empty())
|
||||
{
|
||||
ImGui::TextUnformatted(deleteStatus.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
SceneManager *sceneMgr = eng->_sceneManager.get();
|
||||
// Lights editor (Point + Spot lights)
|
||||
static void ui_lights(VulkanEngine *eng)
|
||||
{
|
||||
if (!eng || !eng->_sceneManager)
|
||||
{
|
||||
ImGui::TextUnformatted("SceneManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager *sceneMgr = eng->_sceneManager.get();
|
||||
|
||||
// Point light editor
|
||||
ImGui::TextUnformatted("Point lights");
|
||||
const auto &lights = sceneMgr->getPointLights();
|
||||
ImGui::Text("Active lights: %zu", lights.size());
|
||||
|
||||
@@ -1730,62 +1919,21 @@ namespace
|
||||
sceneMgr->clearSpotLights();
|
||||
selectedSpot = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Picking & Gizmo - picking info and transform editor
|
||||
static void ui_picking_gizmo(VulkanEngine *eng)
|
||||
{
|
||||
if (!eng || !eng->_sceneManager)
|
||||
{
|
||||
ImGui::TextUnformatted("SceneManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
// Delete selected model/primitive (uses last pick if valid, otherwise hover)
|
||||
static std::string deleteStatus;
|
||||
if (ImGui::Button("Delete selected"))
|
||||
{
|
||||
deleteStatus.clear();
|
||||
const PickingSystem::PickInfo *pick = nullptr;
|
||||
if (picking)
|
||||
{
|
||||
const auto &last = picking->last_pick();
|
||||
const auto &hover = picking->hover_pick();
|
||||
pick = last.valid ? &last : (hover.valid ? &hover : nullptr);
|
||||
}
|
||||
if (!pick || pick->ownerName.empty())
|
||||
{
|
||||
deleteStatus = "No selection to delete.";
|
||||
}
|
||||
else if (pick->ownerType == RenderObject::OwnerType::MeshInstance)
|
||||
{
|
||||
bool ok = eng->_sceneManager->removeMeshInstance(pick->ownerName);
|
||||
if (ok && picking)
|
||||
{
|
||||
picking->clear_owner_picks(RenderObject::OwnerType::MeshInstance, pick->ownerName);
|
||||
}
|
||||
deleteStatus = ok ? "Removed mesh instance: " + pick->ownerName
|
||||
: "Mesh instance not found: " + pick->ownerName;
|
||||
}
|
||||
else if (pick->ownerType == RenderObject::OwnerType::GLTFInstance)
|
||||
{
|
||||
bool ok = eng->_sceneManager->removeGLTFInstance(pick->ownerName);
|
||||
if (ok)
|
||||
{
|
||||
deleteStatus = "Removed glTF instance: " + pick->ownerName;
|
||||
if (picking)
|
||||
{
|
||||
picking->clear_owner_picks(RenderObject::OwnerType::GLTFInstance, pick->ownerName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deleteStatus = "glTF instance not found: " + pick->ownerName;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
deleteStatus = "Cannot delete this object type (static scene).";
|
||||
}
|
||||
}
|
||||
if (!deleteStatus.empty())
|
||||
{
|
||||
ImGui::TextUnformatted(deleteStatus.c_str());
|
||||
}
|
||||
ImGui::Separator();
|
||||
SceneManager *sceneMgr = eng->_sceneManager.get();
|
||||
PickingSystem *picking = eng->picking();
|
||||
|
||||
// Last pick info
|
||||
if (picking && picking->last_pick().valid)
|
||||
{
|
||||
const auto &last = picking->last_pick();
|
||||
@@ -1866,14 +2014,6 @@ namespace
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Object Gizmo (ImGuizmo)");
|
||||
|
||||
if (!eng->_sceneManager)
|
||||
{
|
||||
ImGui::TextUnformatted("SceneManager not available");
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager *sceneMgr = eng->_sceneManager.get();
|
||||
|
||||
// Choose a pick to edit: prefer last pick, then hover.
|
||||
PickingSystem::PickInfo *pick = nullptr;
|
||||
if (picking)
|
||||
@@ -2034,27 +2174,206 @@ namespace
|
||||
pick->worldPos = local_to_world(glm::vec3(targetTransform[3]), sceneMgr->get_world_origin());
|
||||
}
|
||||
}
|
||||
|
||||
static void ui_planets(VulkanEngine *eng)
|
||||
{
|
||||
if (!eng || !eng->_sceneManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager *scene = eng->_sceneManager.get();
|
||||
PlanetSystem *planets = scene->get_planet_system();
|
||||
if (!planets)
|
||||
{
|
||||
ImGui::TextUnformatted("Planet system not available");
|
||||
return;
|
||||
}
|
||||
|
||||
bool enabled = planets->enabled();
|
||||
if (ImGui::Checkbox("Enable planet rendering", &enabled))
|
||||
{
|
||||
planets->set_enabled(enabled);
|
||||
}
|
||||
|
||||
const WorldVec3 origin_world = scene->get_world_origin();
|
||||
const WorldVec3 cam_world = scene->getMainCamera().position_world;
|
||||
const glm::vec3 cam_local = scene->get_camera_local_position();
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Camera world (m): %.3f, %.3f, %.3f", cam_world.x, cam_world.y, cam_world.z);
|
||||
ImGui::Text("Camera local (m): %.3f, %.3f, %.3f", cam_local.x, cam_local.y, cam_local.z);
|
||||
ImGui::Text("World origin (m): %.3f, %.3f, %.3f", origin_world.x, origin_world.y, origin_world.z);
|
||||
|
||||
auto look_at_world = [](Camera &cam, const WorldVec3 &target_world)
|
||||
{
|
||||
glm::dvec3 dirD = glm::normalize(target_world - cam.position_world);
|
||||
glm::vec3 dir = glm::normalize(glm::vec3(dirD));
|
||||
|
||||
glm::vec3 up(0.0f, 1.0f, 0.0f);
|
||||
if (glm::length2(glm::cross(dir, up)) < 1e-6f)
|
||||
{
|
||||
up = glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
|
||||
glm::vec3 f = dir;
|
||||
glm::vec3 r = glm::normalize(glm::cross(up, f));
|
||||
glm::vec3 u = glm::cross(f, r);
|
||||
|
||||
glm::mat3 rot;
|
||||
rot[0] = r;
|
||||
rot[1] = u;
|
||||
rot[2] = -f; // -Z forward
|
||||
cam.orientation = glm::quat_cast(rot);
|
||||
};
|
||||
|
||||
PlanetSystem::PlanetBody *earth = planets->get_body(PlanetSystem::BodyID::Earth);
|
||||
PlanetSystem::PlanetBody *moon = planets->get_body(PlanetSystem::BodyID::Moon);
|
||||
|
||||
if (earth)
|
||||
{
|
||||
ImGui::Separator();
|
||||
|
||||
bool vis = earth->visible;
|
||||
if (ImGui::Checkbox("Render Earth", &vis))
|
||||
{
|
||||
earth->visible = vis;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("(R=%.1f km)", earth->radius_m / 1000.0);
|
||||
|
||||
const double dist = glm::length(cam_world - earth->center_world);
|
||||
const double alt_m = dist - earth->radius_m;
|
||||
ImGui::Text("Altitude above Earth: %.3f km", alt_m / 1000.0);
|
||||
|
||||
if (ImGui::Button("Teleport: 10000 km above surface"))
|
||||
{
|
||||
scene->getMainCamera().position_world =
|
||||
earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e7);
|
||||
look_at_world(scene->getMainCamera(), earth->center_world);
|
||||
}
|
||||
|
||||
if (ImGui::Button("Teleport: 1000 km orbit"))
|
||||
{
|
||||
scene->getMainCamera().position_world =
|
||||
earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e6);
|
||||
look_at_world(scene->getMainCamera(), earth->center_world);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Teleport: 10 km above surface"))
|
||||
{
|
||||
scene->getMainCamera().position_world =
|
||||
earth->center_world + WorldVec3(0.0, 0.0, earth->radius_m + 1.0e4);
|
||||
look_at_world(scene->getMainCamera(), earth->center_world);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
if (ImGui::CollapsingHeader("Earth LOD / Perf", ImGuiTreeNodeFlags_DefaultOpen))
|
||||
{
|
||||
auto settings = planets->earth_quadtree_settings();
|
||||
bool changed = false;
|
||||
|
||||
bool tint = planets->earth_debug_tint_patches_by_lod();
|
||||
if (ImGui::Checkbox("Debug: tint patches by LOD", &tint))
|
||||
{
|
||||
planets->set_earth_debug_tint_patches_by_lod(tint);
|
||||
}
|
||||
|
||||
int maxLevel = static_cast<int>(settings.max_level);
|
||||
if (ImGui::SliderInt("Max LOD level", &maxLevel, 0, 20))
|
||||
{
|
||||
settings.max_level = static_cast<uint32_t>(std::max(0, maxLevel));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (ImGui::SliderFloat("Target SSE (px)", &settings.target_sse_px, 4.0f, 128.0f, "%.1f"))
|
||||
{
|
||||
settings.target_sse_px = std::max(settings.target_sse_px, 0.1f);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
int maxPatches = static_cast<int>(settings.max_patches_visible);
|
||||
if (ImGui::SliderInt("Max visible patches", &maxPatches, 64, 20000))
|
||||
{
|
||||
settings.max_patches_visible = static_cast<uint32_t>(std::max(6, maxPatches));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
int createBudget = static_cast<int>(planets->earth_patch_create_budget_per_frame());
|
||||
if (ImGui::SliderInt("Patch create budget/frame", &createBudget, 0, 512))
|
||||
{
|
||||
planets->set_earth_patch_create_budget_per_frame(static_cast<uint32_t>(std::max(0, createBudget)));
|
||||
}
|
||||
|
||||
float createBudgetMs = planets->earth_patch_create_budget_ms();
|
||||
if (ImGui::DragFloat("Patch create budget (ms)", &createBudgetMs, 0.25f, 0.0f, 50.0f, "%.2f"))
|
||||
{
|
||||
planets->set_earth_patch_create_budget_ms(std::max(0.0f, createBudgetMs));
|
||||
}
|
||||
|
||||
int cacheMax = static_cast<int>(planets->earth_patch_cache_max());
|
||||
if (ImGui::SliderInt("Patch cache max", &cacheMax, 0, 50000))
|
||||
{
|
||||
planets->set_earth_patch_cache_max(static_cast<uint32_t>(std::max(0, cacheMax)));
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Frustum cull", &settings.frustum_cull)) changed = true;
|
||||
if (ImGui::Checkbox("Horizon cull", &settings.horizon_cull)) changed = true;
|
||||
|
||||
if (ImGui::Checkbox("RT guardrail (LOD floor)", &settings.rt_guardrail)) changed = true;
|
||||
if (settings.rt_guardrail)
|
||||
{
|
||||
float maxEdge = static_cast<float>(settings.max_patch_edge_rt_m);
|
||||
if (ImGui::DragFloat("RT max patch edge (m)", &maxEdge, 100.0f, 0.0f, 200000.0f, "%.0f"))
|
||||
{
|
||||
settings.max_patch_edge_rt_m = static_cast<double>(std::max(0.0f, maxEdge));
|
||||
changed = true;
|
||||
}
|
||||
|
||||
float maxAlt = static_cast<float>(settings.rt_guardrail_max_altitude_m);
|
||||
if (ImGui::DragFloat("RT max altitude (m)", &maxAlt, 1000.0f, 0.0f, 2.0e6f, "%.0f"))
|
||||
{
|
||||
settings.rt_guardrail_max_altitude_m = static_cast<double>(std::max(0.0f, maxAlt));
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
planets->set_earth_quadtree_settings(settings);
|
||||
}
|
||||
|
||||
const PlanetSystem::EarthDebugStats &s = planets->earth_debug_stats();
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Visible patches: %u (est. tris: %u)", s.visible_patches, s.estimated_triangles);
|
||||
ImGui::Text("Cache size: %u (created this frame: %u)", s.patch_cache_size, s.created_patches);
|
||||
ImGui::Text("Quadtree: max level used %u | visited %u | culled %u | budget-limited %u",
|
||||
s.quadtree.max_level_used,
|
||||
s.quadtree.nodes_visited,
|
||||
s.quadtree.nodes_culled,
|
||||
s.quadtree.splits_budget_limited);
|
||||
ImGui::Text("CPU ms: quadtree %.2f | create %.2f | emit %.2f | total %.2f",
|
||||
s.ms_quadtree, s.ms_patch_create, s.ms_emit, s.ms_total);
|
||||
}
|
||||
}
|
||||
|
||||
if (moon)
|
||||
{
|
||||
bool vis = moon->visible;
|
||||
if (ImGui::Checkbox("Render Moon", &vis))
|
||||
{
|
||||
moon->visible = vis;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("(R=%.1f km)", moon->radius_m / 1000.0);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Window visibility states for menu-bar toggles
|
||||
namespace
|
||||
{
|
||||
struct DebugWindowStates
|
||||
{
|
||||
bool show_overview{false};
|
||||
bool show_window{false};
|
||||
bool show_background{false};
|
||||
bool show_particles{false};
|
||||
bool show_shadows{false};
|
||||
bool show_render_graph{false};
|
||||
bool show_pipelines{false};
|
||||
bool show_ibl{false};
|
||||
bool show_postfx{false};
|
||||
bool show_scene{false};
|
||||
bool show_async_assets{false};
|
||||
bool show_textures{false};
|
||||
};
|
||||
static DebugWindowStates g_debug_windows;
|
||||
static bool g_show_debug_window = false;
|
||||
} // namespace
|
||||
|
||||
void vk_engine_draw_debug_ui(VulkanEngine *eng)
|
||||
@@ -2068,21 +2387,7 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng)
|
||||
{
|
||||
if (ImGui::BeginMenu("View"))
|
||||
{
|
||||
ImGui::MenuItem("Overview", nullptr, &g_debug_windows.show_overview);
|
||||
ImGui::MenuItem("Window", nullptr, &g_debug_windows.show_window);
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem("Scene", nullptr, &g_debug_windows.show_scene);
|
||||
ImGui::MenuItem("Render Graph", nullptr, &g_debug_windows.show_render_graph);
|
||||
ImGui::MenuItem("Pipelines", nullptr, &g_debug_windows.show_pipelines);
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem("Shadows", nullptr, &g_debug_windows.show_shadows);
|
||||
ImGui::MenuItem("IBL", nullptr, &g_debug_windows.show_ibl);
|
||||
ImGui::MenuItem("PostFX", nullptr, &g_debug_windows.show_postfx);
|
||||
ImGui::MenuItem("Background", nullptr, &g_debug_windows.show_background);
|
||||
ImGui::Separator();
|
||||
ImGui::MenuItem("Particles", nullptr, &g_debug_windows.show_particles);
|
||||
ImGui::MenuItem("Textures", nullptr, &g_debug_windows.show_textures);
|
||||
ImGui::MenuItem("Async Assets", nullptr, &g_debug_windows.show_async_assets);
|
||||
ImGui::MenuItem("Engine Debug", nullptr, &g_show_debug_window);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
@@ -2096,111 +2401,112 @@ void vk_engine_draw_debug_ui(VulkanEngine *eng)
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
// Individual debug windows (only shown when toggled)
|
||||
if (g_debug_windows.show_overview)
|
||||
// Single consolidated debug window with tabs
|
||||
if (g_show_debug_window)
|
||||
{
|
||||
if (ImGui::Begin("Overview", &g_debug_windows.show_overview))
|
||||
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
|
||||
if (ImGui::Begin("Engine Debug", &g_show_debug_window))
|
||||
{
|
||||
ui_overview(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabBar("DebugTabs", ImGuiTabBarFlags_None))
|
||||
{
|
||||
if (ImGui::BeginTabItem("Overview"))
|
||||
{
|
||||
ui_overview(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_window)
|
||||
{
|
||||
if (ImGui::Begin("Window Settings", &g_debug_windows.show_window))
|
||||
{
|
||||
ui_window(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Scene Editor"))
|
||||
{
|
||||
ui_scene_editor(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_background)
|
||||
{
|
||||
if (ImGui::Begin("Background", &g_debug_windows.show_background))
|
||||
{
|
||||
ui_background(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Lights"))
|
||||
{
|
||||
ui_lights(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_particles)
|
||||
{
|
||||
if (ImGui::Begin("Particles", &g_debug_windows.show_particles))
|
||||
{
|
||||
ui_particles(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Picking & Gizmo"))
|
||||
{
|
||||
ui_picking_gizmo(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_shadows)
|
||||
{
|
||||
if (ImGui::Begin("Shadows", &g_debug_windows.show_shadows))
|
||||
{
|
||||
ui_shadows(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Camera"))
|
||||
{
|
||||
ui_camera(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_render_graph)
|
||||
{
|
||||
if (ImGui::Begin("Render Graph", &g_debug_windows.show_render_graph))
|
||||
{
|
||||
ui_render_graph(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Planets"))
|
||||
{
|
||||
ui_planets(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_pipelines)
|
||||
{
|
||||
if (ImGui::Begin("Pipelines", &g_debug_windows.show_pipelines))
|
||||
{
|
||||
ui_pipelines(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Render Graph"))
|
||||
{
|
||||
ui_render_graph(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_ibl)
|
||||
{
|
||||
if (ImGui::Begin("IBL", &g_debug_windows.show_ibl))
|
||||
{
|
||||
ui_ibl(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Pipelines"))
|
||||
{
|
||||
ui_pipelines(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_postfx)
|
||||
{
|
||||
if (ImGui::Begin("PostFX", &g_debug_windows.show_postfx))
|
||||
{
|
||||
ui_postfx(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Shadows"))
|
||||
{
|
||||
ui_shadows(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_scene)
|
||||
{
|
||||
if (ImGui::Begin("Scene", &g_debug_windows.show_scene))
|
||||
{
|
||||
ui_scene(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("IBL"))
|
||||
{
|
||||
ui_ibl(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_async_assets)
|
||||
{
|
||||
if (ImGui::Begin("Async Assets", &g_debug_windows.show_async_assets))
|
||||
{
|
||||
ui_async_assets(eng);
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
if (ImGui::BeginTabItem("PostFX"))
|
||||
{
|
||||
ui_postfx(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (g_debug_windows.show_textures)
|
||||
{
|
||||
if (ImGui::Begin("Textures", &g_debug_windows.show_textures))
|
||||
{
|
||||
ui_textures(eng);
|
||||
if (ImGui::BeginTabItem("Background"))
|
||||
{
|
||||
ui_background(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Particles"))
|
||||
{
|
||||
ui_particles(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Window"))
|
||||
{
|
||||
ui_window(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Textures"))
|
||||
{
|
||||
ui_textures(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (ImGui::BeginTabItem("Async Assets"))
|
||||
{
|
||||
ui_async_assets(eng);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "core/picking/picking_system.h"
|
||||
#include "scene/vk_scene.h"
|
||||
#include "scene/camera.h"
|
||||
#include "scene/camera/camera_rig.h"
|
||||
|
||||
#include <glm/gtx/matrix_decompose.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
@@ -1579,6 +1580,268 @@ void Engine::camera_look_at(const glm::dvec3& target)
|
||||
cam.orientation = glm::quat_cast(rot);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
::CameraMode to_internal_camera_mode(GameAPI::CameraMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case GameAPI::CameraMode::Free: return ::CameraMode::Free;
|
||||
case GameAPI::CameraMode::Orbit: return ::CameraMode::Orbit;
|
||||
case GameAPI::CameraMode::Follow: return ::CameraMode::Follow;
|
||||
case GameAPI::CameraMode::Chase: return ::CameraMode::Chase;
|
||||
case GameAPI::CameraMode::Fixed: return ::CameraMode::Fixed;
|
||||
default: return ::CameraMode::Free;
|
||||
}
|
||||
}
|
||||
|
||||
GameAPI::CameraMode to_api_camera_mode(::CameraMode mode)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case ::CameraMode::Free: return GameAPI::CameraMode::Free;
|
||||
case ::CameraMode::Orbit: return GameAPI::CameraMode::Orbit;
|
||||
case ::CameraMode::Follow: return GameAPI::CameraMode::Follow;
|
||||
case ::CameraMode::Chase: return GameAPI::CameraMode::Chase;
|
||||
case ::CameraMode::Fixed: return GameAPI::CameraMode::Fixed;
|
||||
default: return GameAPI::CameraMode::Free;
|
||||
}
|
||||
}
|
||||
|
||||
::CameraTargetType to_internal_target_type(GameAPI::CameraTargetType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GameAPI::CameraTargetType::None: return ::CameraTargetType::None;
|
||||
case GameAPI::CameraTargetType::WorldPoint: return ::CameraTargetType::WorldPoint;
|
||||
case GameAPI::CameraTargetType::MeshInstance: return ::CameraTargetType::MeshInstance;
|
||||
case GameAPI::CameraTargetType::GLTFInstance: return ::CameraTargetType::GLTFInstance;
|
||||
default: return ::CameraTargetType::None;
|
||||
}
|
||||
}
|
||||
|
||||
GameAPI::CameraTargetType to_api_target_type(::CameraTargetType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ::CameraTargetType::None: return GameAPI::CameraTargetType::None;
|
||||
case ::CameraTargetType::WorldPoint: return GameAPI::CameraTargetType::WorldPoint;
|
||||
case ::CameraTargetType::MeshInstance: return GameAPI::CameraTargetType::MeshInstance;
|
||||
case ::CameraTargetType::GLTFInstance: return GameAPI::CameraTargetType::GLTFInstance;
|
||||
default: return GameAPI::CameraTargetType::None;
|
||||
}
|
||||
}
|
||||
|
||||
::CameraTarget to_internal_target(const GameAPI::CameraTarget &target)
|
||||
{
|
||||
::CameraTarget t;
|
||||
t.type = to_internal_target_type(target.type);
|
||||
t.name = target.name;
|
||||
t.world_point = WorldVec3(target.worldPoint);
|
||||
return t;
|
||||
}
|
||||
|
||||
GameAPI::CameraTarget to_api_target(const ::CameraTarget &target)
|
||||
{
|
||||
GameAPI::CameraTarget t;
|
||||
t.type = to_api_target_type(target.type);
|
||||
t.name = target.name;
|
||||
t.worldPoint = glm::dvec3(target.world_point);
|
||||
return t;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void Engine::set_camera_mode(CameraMode mode)
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SceneManager *scene = _engine->_sceneManager.get();
|
||||
Camera &cam = scene->getMainCamera();
|
||||
CameraRig &rig = scene->getCameraRig();
|
||||
|
||||
if (_engine->_input)
|
||||
{
|
||||
_engine->_input->set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
|
||||
rig.set_mode(to_internal_camera_mode(mode), *scene, cam);
|
||||
}
|
||||
|
||||
CameraMode Engine::get_camera_mode() const
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return CameraMode::Free;
|
||||
}
|
||||
return to_api_camera_mode(_engine->_sceneManager->getCameraRig().mode());
|
||||
}
|
||||
|
||||
void Engine::set_free_camera_settings(const FreeCameraSettings& settings)
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
::FreeCameraSettings &s = _engine->_sceneManager->getCameraRig().free_settings();
|
||||
s.move_speed = settings.moveSpeed;
|
||||
s.look_sensitivity = settings.lookSensitivity;
|
||||
s.roll_speed = settings.rollSpeed;
|
||||
}
|
||||
|
||||
FreeCameraSettings Engine::get_free_camera_settings() const
|
||||
{
|
||||
FreeCameraSettings out{};
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return out;
|
||||
}
|
||||
|
||||
const ::FreeCameraSettings &s = _engine->_sceneManager->getCameraRig().free_settings();
|
||||
out.moveSpeed = s.move_speed;
|
||||
out.lookSensitivity = s.look_sensitivity;
|
||||
out.rollSpeed = s.roll_speed;
|
||||
return out;
|
||||
}
|
||||
|
||||
void Engine::set_orbit_camera_settings(const OrbitCameraSettings& settings)
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
::OrbitCameraSettings &s = _engine->_sceneManager->getCameraRig().orbit_settings();
|
||||
s.target = to_internal_target(settings.target);
|
||||
s.distance = settings.distance;
|
||||
s.yaw = settings.yaw;
|
||||
s.pitch = settings.pitch;
|
||||
s.look_sensitivity = settings.lookSensitivity;
|
||||
}
|
||||
|
||||
OrbitCameraSettings Engine::get_orbit_camera_settings() const
|
||||
{
|
||||
OrbitCameraSettings out{};
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return out;
|
||||
}
|
||||
|
||||
const ::OrbitCameraSettings &s = _engine->_sceneManager->getCameraRig().orbit_settings();
|
||||
out.target = to_api_target(s.target);
|
||||
out.distance = s.distance;
|
||||
out.yaw = s.yaw;
|
||||
out.pitch = s.pitch;
|
||||
out.lookSensitivity = s.look_sensitivity;
|
||||
return out;
|
||||
}
|
||||
|
||||
void Engine::set_follow_camera_settings(const FollowCameraSettings& settings)
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
::FollowCameraSettings &s = _engine->_sceneManager->getCameraRig().follow_settings();
|
||||
s.target = to_internal_target(settings.target);
|
||||
s.position_offset_local = settings.positionOffsetLocal;
|
||||
s.rotation_offset = settings.rotationOffset;
|
||||
}
|
||||
|
||||
FollowCameraSettings Engine::get_follow_camera_settings() const
|
||||
{
|
||||
FollowCameraSettings out{};
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return out;
|
||||
}
|
||||
|
||||
const ::FollowCameraSettings &s = _engine->_sceneManager->getCameraRig().follow_settings();
|
||||
out.target = to_api_target(s.target);
|
||||
out.positionOffsetLocal = s.position_offset_local;
|
||||
out.rotationOffset = s.rotation_offset;
|
||||
return out;
|
||||
}
|
||||
|
||||
void Engine::set_chase_camera_settings(const ChaseCameraSettings& settings)
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
::ChaseCameraSettings &s = _engine->_sceneManager->getCameraRig().chase_settings();
|
||||
s.target = to_internal_target(settings.target);
|
||||
s.position_offset_local = settings.positionOffsetLocal;
|
||||
s.rotation_offset = settings.rotationOffset;
|
||||
s.position_lag = settings.positionLag;
|
||||
s.rotation_lag = settings.rotationLag;
|
||||
}
|
||||
|
||||
ChaseCameraSettings Engine::get_chase_camera_settings() const
|
||||
{
|
||||
ChaseCameraSettings out{};
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return out;
|
||||
}
|
||||
|
||||
const ::ChaseCameraSettings &s = _engine->_sceneManager->getCameraRig().chase_settings();
|
||||
out.target = to_api_target(s.target);
|
||||
out.positionOffsetLocal = s.position_offset_local;
|
||||
out.rotationOffset = s.rotation_offset;
|
||||
out.positionLag = s.position_lag;
|
||||
out.rotationLag = s.rotation_lag;
|
||||
return out;
|
||||
}
|
||||
|
||||
bool Engine::set_camera_target_from_last_pick()
|
||||
{
|
||||
if (!_engine || !_engine->_sceneManager)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const PickingSystem *picking = _engine->picking();
|
||||
if (!picking)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &pick = picking->last_pick();
|
||||
if (!pick.valid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
::CameraTarget t;
|
||||
if (pick.ownerType == RenderObject::OwnerType::MeshInstance)
|
||||
{
|
||||
t.type = ::CameraTargetType::MeshInstance;
|
||||
t.name = pick.ownerName;
|
||||
}
|
||||
else if (pick.ownerType == RenderObject::OwnerType::GLTFInstance)
|
||||
{
|
||||
t.type = ::CameraTargetType::GLTFInstance;
|
||||
t.name = pick.ownerName;
|
||||
}
|
||||
else
|
||||
{
|
||||
t.type = ::CameraTargetType::WorldPoint;
|
||||
t.world_point = pick.worldPos;
|
||||
}
|
||||
|
||||
CameraRig &rig = _engine->_sceneManager->getCameraRig();
|
||||
rig.orbit_settings().target = t;
|
||||
rig.follow_settings().target = t;
|
||||
rig.chase_settings().target = t;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
@@ -304,6 +304,66 @@ struct Stats
|
||||
int drawCallCount{0};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Camera Rig Types
|
||||
// ============================================================================
|
||||
|
||||
enum class CameraMode : uint8_t
|
||||
{
|
||||
Free = 0,
|
||||
Orbit = 1,
|
||||
Follow = 2,
|
||||
Chase = 3,
|
||||
Fixed = 4
|
||||
};
|
||||
|
||||
enum class CameraTargetType : uint8_t
|
||||
{
|
||||
None = 0,
|
||||
WorldPoint = 1,
|
||||
MeshInstance = 2,
|
||||
GLTFInstance = 3
|
||||
};
|
||||
|
||||
struct CameraTarget
|
||||
{
|
||||
CameraTargetType type{CameraTargetType::None};
|
||||
std::string name{};
|
||||
glm::dvec3 worldPoint{0.0, 0.0, 0.0};
|
||||
};
|
||||
|
||||
struct FreeCameraSettings
|
||||
{
|
||||
float moveSpeed{1.8f}; // world units / second
|
||||
float lookSensitivity{0.0020f};
|
||||
float rollSpeed{1.0f}; // radians / second
|
||||
};
|
||||
|
||||
struct OrbitCameraSettings
|
||||
{
|
||||
CameraTarget target{};
|
||||
double distance{10.0};
|
||||
float yaw{0.0f}; // radians
|
||||
float pitch{0.0f}; // radians
|
||||
float lookSensitivity{0.0020f};
|
||||
};
|
||||
|
||||
struct FollowCameraSettings
|
||||
{
|
||||
CameraTarget target{};
|
||||
glm::vec3 positionOffsetLocal{0.0f, 2.0f, 6.0f};
|
||||
glm::quat rotationOffset{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
struct ChaseCameraSettings
|
||||
{
|
||||
CameraTarget target{};
|
||||
glm::vec3 positionOffsetLocal{0.0f, 2.0f, 6.0f};
|
||||
glm::quat rotationOffset{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
float positionLag{8.0f}; // smoothing rate (1/sec), higher = snappier
|
||||
float rotationLag{10.0f}; // smoothing rate (1/sec)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Main API Class
|
||||
// ============================================================================
|
||||
@@ -651,6 +711,25 @@ public:
|
||||
void camera_look_at(const glm::vec3& target);
|
||||
void camera_look_at(const glm::dvec3& target);
|
||||
|
||||
// Camera mode and per-mode settings
|
||||
void set_camera_mode(CameraMode mode);
|
||||
CameraMode get_camera_mode() const;
|
||||
|
||||
void set_free_camera_settings(const FreeCameraSettings& settings);
|
||||
FreeCameraSettings get_free_camera_settings() const;
|
||||
|
||||
void set_orbit_camera_settings(const OrbitCameraSettings& settings);
|
||||
OrbitCameraSettings get_orbit_camera_settings() const;
|
||||
|
||||
void set_follow_camera_settings(const FollowCameraSettings& settings);
|
||||
FollowCameraSettings get_follow_camera_settings() const;
|
||||
|
||||
void set_chase_camera_settings(const ChaseCameraSettings& settings);
|
||||
ChaseCameraSettings get_chase_camera_settings() const;
|
||||
|
||||
// Convenience: set Orbit/Follow/Chase target from the engine's last pick.
|
||||
bool set_camera_target_from_last_pick();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
@@ -29,6 +29,13 @@ void SamplerManager::init(DeviceManager *deviceManager)
|
||||
sampl.minFilter = VK_FILTER_LINEAR;
|
||||
vkCreateSampler(_deviceManager->device(), &sampl, nullptr, &_defaultSamplerLinear);
|
||||
|
||||
// Linear clamp-to-edge (useful for tiled textures)
|
||||
VkSamplerCreateInfo clampEdge = sampl;
|
||||
clampEdge.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
clampEdge.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
clampEdge.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
vkCreateSampler(_deviceManager->device(), &clampEdge, nullptr, &_linearClampEdge);
|
||||
|
||||
// Shadow linear clamp sampler (border=white)
|
||||
VkSamplerCreateInfo sh = sampl;
|
||||
sh.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
|
||||
@@ -60,4 +67,10 @@ void SamplerManager::cleanup()
|
||||
vkDestroySampler(_deviceManager->device(), _shadowLinearClamp, nullptr);
|
||||
_shadowLinearClamp = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
if (_linearClampEdge)
|
||||
{
|
||||
vkDestroySampler(_deviceManager->device(), _linearClampEdge, nullptr);
|
||||
_linearClampEdge = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public:
|
||||
VkSampler defaultLinear() const { return _defaultSamplerLinear; }
|
||||
VkSampler defaultNearest() const { return _defaultSamplerNearest; }
|
||||
VkSampler shadowLinearClamp() const { return _shadowLinearClamp; }
|
||||
VkSampler linearClampEdge() const { return _linearClampEdge; }
|
||||
|
||||
|
||||
private:
|
||||
@@ -21,4 +22,5 @@ private:
|
||||
VkSampler _defaultSamplerLinear = VK_NULL_HANDLE;
|
||||
VkSampler _defaultSamplerNearest = VK_NULL_HANDLE;
|
||||
VkSampler _shadowLinearClamp = VK_NULL_HANDLE;
|
||||
VkSampler _linearClampEdge = VK_NULL_HANDLE;
|
||||
};
|
||||
|
||||
10
src/main.cpp
10
src/main.cpp
@@ -33,6 +33,16 @@ public:
|
||||
api.load_global_ibl(ibl);
|
||||
}
|
||||
|
||||
// Planet demo defaults (Milestone A): start outside Earth and speed up the free camera.
|
||||
{
|
||||
constexpr double kEarthRadiusM = 6378137.0;
|
||||
GameAPI::FreeCameraSettings free = api.get_free_camera_settings();
|
||||
free.moveSpeed = 20000.0f;
|
||||
api.set_free_camera_settings(free);
|
||||
|
||||
api.set_camera_position(glm::dvec3(0.0, 0.0, kEarthRadiusM + 1.0e6));
|
||||
api.camera_look_at(glm::dvec3(0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Load a glTF model asynchronously
|
||||
// api.add_gltf_instance_async("example_model", "models/example.gltf",
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace GameRuntime
|
||||
// --- Camera input (if not captured by UI) ---
|
||||
if (_renderer->_sceneManager && input)
|
||||
{
|
||||
_renderer->_sceneManager->getMainCamera().process_input(*input, ui_capture_keyboard, ui_capture_mouse);
|
||||
_renderer->_sceneManager->getCameraRig().process_input(*input, ui_capture_keyboard, ui_capture_mouse);
|
||||
}
|
||||
|
||||
// --- Throttle when minimized ---
|
||||
|
||||
@@ -1,102 +1,6 @@
|
||||
#include "camera.h"
|
||||
#include <glm/gtx/transform.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
void Camera::update()
|
||||
{
|
||||
glm::mat4 cameraRotation = getRotationMatrix();
|
||||
glm::vec3 delta = glm::vec3(cameraRotation * glm::vec4(velocity * moveSpeed, 0.f));
|
||||
position_world += glm::dvec3(delta);
|
||||
}
|
||||
|
||||
void Camera::process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse)
|
||||
{
|
||||
const InputState &st = input.state();
|
||||
|
||||
// Movement is state-based so simultaneous keys work naturally.
|
||||
if (ui_capture_keyboard)
|
||||
{
|
||||
velocity = glm::vec3(0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
glm::vec3 v(0.0f);
|
||||
if (st.key_down(Key::W)) { v.z -= 1.0f; }
|
||||
if (st.key_down(Key::S)) { v.z += 1.0f; }
|
||||
if (st.key_down(Key::A)) { v.x -= 1.0f; }
|
||||
if (st.key_down(Key::D)) { v.x += 1.0f; }
|
||||
velocity = v;
|
||||
}
|
||||
|
||||
// Event-based mouse handling so we don't apply motion that happened before RMB was pressed in the same frame.
|
||||
for (const InputEvent &e : input.events())
|
||||
{
|
||||
if (ui_capture_mouse)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type == InputEvent::Type::MouseButtonDown && e.mouse_button == MouseButton::Right)
|
||||
{
|
||||
rmbDown = true;
|
||||
input.set_cursor_mode(CursorMode::Relative);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseButtonUp && e.mouse_button == MouseButton::Right)
|
||||
{
|
||||
rmbDown = false;
|
||||
input.set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseMove && rmbDown)
|
||||
{
|
||||
// Convert mouse motion to incremental yaw/pitch angles.
|
||||
float dx = e.mouse_delta.x * lookSensitivity;
|
||||
float dy = e.mouse_delta.y * lookSensitivity;
|
||||
|
||||
// Mouse right (xrel > 0) turns view right with -Z-forward: yaw around +Y.
|
||||
glm::quat yawRotation = glm::angleAxis(dx, glm::vec3 { 0.f, 1.f, 0.f });
|
||||
|
||||
// Mouse up (yrel < 0) looks up with -Z-forward: negative dy.
|
||||
float pitchDelta = -dy;
|
||||
// Pitch around the camera's local X (right) axis in world space.
|
||||
glm::vec3 right = glm::rotate(orientation, glm::vec3 { 1.f, 0.f, 0.f });
|
||||
glm::quat pitchRotation = glm::angleAxis(pitchDelta, glm::vec3(right));
|
||||
|
||||
// Apply yaw, then pitch, to the current orientation.
|
||||
orientation = glm::normalize(pitchRotation * yawRotation * orientation);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseWheel)
|
||||
{
|
||||
const float steps = e.wheel_delta.y; // positive = wheel up
|
||||
if (std::abs(steps) < 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ctrl modifies FOV, otherwise adjust move speed
|
||||
if (e.mods.ctrl)
|
||||
{
|
||||
// Wheel up -> zoom in (smaller FOV)
|
||||
fovDegrees -= steps * 2.0f;
|
||||
fovDegrees = std::clamp(fovDegrees, 30.0f, 110.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Exponential scale for pleasant feel
|
||||
float factor = std::pow(1.15f, steps);
|
||||
moveSpeed = std::clamp(moveSpeed * factor, 0.001f, 5.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: if mouse state shows RMB is no longer down, release relative mode.
|
||||
if (rmbDown && !st.mouse_down(MouseButton::Right))
|
||||
{
|
||||
rmbDown = false;
|
||||
input.set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
glm::mat4 Camera::getViewMatrix(const glm::vec3 &position_local) const
|
||||
{
|
||||
|
||||
@@ -2,29 +2,15 @@
|
||||
|
||||
#include <core/types.h>
|
||||
|
||||
#include <core/input/input_system.h>
|
||||
|
||||
#include "glm/vec3.hpp"
|
||||
|
||||
class Camera {
|
||||
public:
|
||||
glm::vec3 velocity{0.0f, 0.0f, 0.0f};
|
||||
glm::dvec3 position_world{0.0, 0.0, 0.0};
|
||||
// Orientation stored as a quaternion (local -> world).
|
||||
glm::quat orientation { 1.0f, 0.0f, 0.0f, 0.0f };
|
||||
|
||||
// Movement/look tuning
|
||||
float moveSpeed { 0.03f };
|
||||
float lookSensitivity { 0.0020f };
|
||||
bool rmbDown { false };
|
||||
|
||||
// Field of view in degrees for projection
|
||||
float fovDegrees { 50.f };
|
||||
|
||||
glm::mat4 getViewMatrix(const glm::vec3 &position_local) const;
|
||||
glm::mat4 getRotationMatrix() const;
|
||||
|
||||
void process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse);
|
||||
|
||||
void update();
|
||||
};
|
||||
|
||||
129
src/scene/camera/camera_rig.cpp
Normal file
129
src/scene/camera/camera_rig.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "camera_rig.h"
|
||||
|
||||
#include <scene/camera/mode_chase.h>
|
||||
#include <scene/camera/mode_fixed.h>
|
||||
#include <scene/camera/mode_follow.h>
|
||||
#include <scene/camera/mode_free.h>
|
||||
#include <scene/camera/mode_orbit.h>
|
||||
#include <scene/camera.h>
|
||||
#include <scene/vk_scene.h>
|
||||
|
||||
#include <core/input/input_system.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::unique_ptr<ICameraMode> make_mode(CameraMode mode,
|
||||
FreeCameraSettings &free_settings,
|
||||
OrbitCameraSettings &orbit_settings,
|
||||
FollowCameraSettings &follow_settings,
|
||||
ChaseCameraSettings &chase_settings,
|
||||
FixedCameraSettings &fixed_settings)
|
||||
{
|
||||
switch (mode)
|
||||
{
|
||||
case CameraMode::Free: return std::make_unique<FreeCameraMode>(free_settings);
|
||||
case CameraMode::Orbit: return std::make_unique<OrbitCameraMode>(orbit_settings);
|
||||
case CameraMode::Follow: return std::make_unique<FollowCameraMode>(follow_settings);
|
||||
case CameraMode::Chase: return std::make_unique<ChaseCameraMode>(chase_settings);
|
||||
case CameraMode::Fixed: return std::make_unique<FixedCameraMode>(fixed_settings);
|
||||
default: return std::make_unique<FreeCameraMode>(free_settings);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
CameraRig::CameraRig() = default;
|
||||
CameraRig::~CameraRig() = default;
|
||||
|
||||
void CameraRig::init(SceneManager &scene, Camera &camera)
|
||||
{
|
||||
_scene = &scene;
|
||||
_camera = &camera;
|
||||
recreate_mode(scene, camera);
|
||||
}
|
||||
|
||||
void CameraRig::set_mode(CameraMode mode, SceneManager &scene, Camera &camera)
|
||||
{
|
||||
if (_mode == mode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_mode = mode;
|
||||
recreate_mode(scene, camera);
|
||||
}
|
||||
|
||||
const char *CameraRig::mode_name() const
|
||||
{
|
||||
if (_mode_impl)
|
||||
{
|
||||
return _mode_impl->name();
|
||||
}
|
||||
return "None";
|
||||
}
|
||||
|
||||
void CameraRig::process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse)
|
||||
{
|
||||
if (_mode_impl && _scene && _camera)
|
||||
{
|
||||
_mode_impl->process_input(*_scene, *_camera, input, ui_capture_keyboard, ui_capture_mouse);
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRig::update(SceneManager &scene, Camera &camera, float dt)
|
||||
{
|
||||
if (_mode_impl)
|
||||
{
|
||||
_mode_impl->update(scene, camera, dt);
|
||||
}
|
||||
}
|
||||
|
||||
bool CameraRig::resolve_target(SceneManager &scene,
|
||||
const CameraTarget &target,
|
||||
WorldVec3 &out_position_world,
|
||||
glm::quat &out_rotation) const
|
||||
{
|
||||
out_rotation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
switch (target.type)
|
||||
{
|
||||
case CameraTargetType::WorldPoint:
|
||||
out_position_world = target.world_point;
|
||||
return true;
|
||||
case CameraTargetType::MeshInstance:
|
||||
{
|
||||
WorldVec3 t{};
|
||||
glm::quat r{};
|
||||
glm::vec3 s{};
|
||||
if (!scene.getMeshInstanceTRSWorld(target.name, t, r, s))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out_position_world = t;
|
||||
out_rotation = r;
|
||||
return true;
|
||||
}
|
||||
case CameraTargetType::GLTFInstance:
|
||||
{
|
||||
WorldVec3 t{};
|
||||
glm::quat r{};
|
||||
glm::vec3 s{};
|
||||
if (!scene.getGLTFInstanceTRSWorld(target.name, t, r, s))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
out_position_world = t;
|
||||
out_rotation = r;
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRig::recreate_mode(SceneManager &scene, Camera &camera)
|
||||
{
|
||||
_mode_impl = make_mode(_mode, _free, _orbit, _follow, _chase, _fixed);
|
||||
if (_mode_impl)
|
||||
{
|
||||
_mode_impl->on_activate(scene, camera);
|
||||
}
|
||||
}
|
||||
124
src/scene/camera/camera_rig.h
Normal file
124
src/scene/camera/camera_rig.h
Normal file
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/world.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class Camera;
|
||||
class InputSystem;
|
||||
class SceneManager;
|
||||
|
||||
enum class CameraMode : uint8_t
|
||||
{
|
||||
Free = 0,
|
||||
Orbit = 1,
|
||||
Follow = 2,
|
||||
Chase = 3,
|
||||
Fixed = 4
|
||||
};
|
||||
|
||||
enum class CameraTargetType : uint8_t
|
||||
{
|
||||
None = 0,
|
||||
WorldPoint = 1,
|
||||
MeshInstance = 2,
|
||||
GLTFInstance = 3
|
||||
};
|
||||
|
||||
struct CameraTarget
|
||||
{
|
||||
CameraTargetType type{CameraTargetType::None};
|
||||
std::string name{};
|
||||
WorldVec3 world_point{0.0, 0.0, 0.0};
|
||||
};
|
||||
|
||||
struct FreeCameraSettings
|
||||
{
|
||||
float move_speed{1.8f}; // world units / second
|
||||
float look_sensitivity{0.0020f};
|
||||
float roll_speed{1.0f}; // radians / second
|
||||
};
|
||||
|
||||
struct OrbitCameraSettings
|
||||
{
|
||||
CameraTarget target{};
|
||||
double distance{10.0};
|
||||
float yaw{0.0f}; // radians
|
||||
float pitch{0.0f}; // radians
|
||||
float look_sensitivity{0.0020f};
|
||||
};
|
||||
|
||||
struct FollowCameraSettings
|
||||
{
|
||||
CameraTarget target{};
|
||||
glm::vec3 position_offset_local{0.0f, 2.0f, 6.0f};
|
||||
glm::quat rotation_offset{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
struct ChaseCameraSettings
|
||||
{
|
||||
CameraTarget target{};
|
||||
glm::vec3 position_offset_local{0.0f, 2.0f, 6.0f};
|
||||
glm::quat rotation_offset{1.0f, 0.0f, 0.0f, 0.0f};
|
||||
float position_lag{8.0f}; // smoothing rate (1/sec), higher = snappier
|
||||
float rotation_lag{10.0f}; // smoothing rate (1/sec)
|
||||
};
|
||||
|
||||
struct FixedCameraSettings
|
||||
{
|
||||
};
|
||||
|
||||
class CameraRig
|
||||
{
|
||||
public:
|
||||
CameraRig();
|
||||
~CameraRig();
|
||||
|
||||
CameraRig(const CameraRig &) = delete;
|
||||
CameraRig &operator=(const CameraRig &) = delete;
|
||||
|
||||
void init(SceneManager &scene, Camera &camera);
|
||||
|
||||
CameraMode mode() const { return _mode; }
|
||||
void set_mode(CameraMode mode, SceneManager &scene, Camera &camera);
|
||||
|
||||
const char *mode_name() const;
|
||||
|
||||
FreeCameraSettings &free_settings() { return _free; }
|
||||
OrbitCameraSettings &orbit_settings() { return _orbit; }
|
||||
FollowCameraSettings &follow_settings() { return _follow; }
|
||||
ChaseCameraSettings &chase_settings() { return _chase; }
|
||||
FixedCameraSettings &fixed_settings() { return _fixed; }
|
||||
|
||||
const FreeCameraSettings &free_settings() const { return _free; }
|
||||
const OrbitCameraSettings &orbit_settings() const { return _orbit; }
|
||||
const FollowCameraSettings &follow_settings() const { return _follow; }
|
||||
const ChaseCameraSettings &chase_settings() const { return _chase; }
|
||||
const FixedCameraSettings &fixed_settings() const { return _fixed; }
|
||||
|
||||
void process_input(InputSystem &input, bool ui_capture_keyboard, bool ui_capture_mouse);
|
||||
void update(SceneManager &scene, Camera &camera, float dt);
|
||||
|
||||
bool resolve_target(SceneManager &scene,
|
||||
const CameraTarget &target,
|
||||
WorldVec3 &out_position_world,
|
||||
glm::quat &out_rotation) const;
|
||||
|
||||
private:
|
||||
void recreate_mode(SceneManager &scene, Camera &camera);
|
||||
|
||||
CameraMode _mode{CameraMode::Free};
|
||||
std::unique_ptr<class ICameraMode> _mode_impl;
|
||||
SceneManager *_scene = nullptr;
|
||||
Camera *_camera = nullptr;
|
||||
|
||||
FreeCameraSettings _free{};
|
||||
OrbitCameraSettings _orbit{};
|
||||
FollowCameraSettings _follow{};
|
||||
ChaseCameraSettings _chase{};
|
||||
FixedCameraSettings _fixed{};
|
||||
};
|
||||
20
src/scene/camera/icamera_mode.h
Normal file
20
src/scene/camera/icamera_mode.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
class Camera;
|
||||
class InputSystem;
|
||||
class SceneManager;
|
||||
|
||||
class ICameraMode
|
||||
{
|
||||
public:
|
||||
virtual ~ICameraMode() = default;
|
||||
|
||||
virtual const char *name() const = 0;
|
||||
virtual void on_activate(SceneManager &scene, Camera &camera) = 0;
|
||||
virtual void process_input(SceneManager &scene,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse) = 0;
|
||||
virtual void update(SceneManager &scene, Camera &camera, float dt) = 0;
|
||||
};
|
||||
74
src/scene/camera/mode_chase.cpp
Normal file
74
src/scene/camera/mode_chase.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include "mode_chase.h"
|
||||
|
||||
#include <scene/camera/camera_rig.h>
|
||||
#include <scene/camera.h>
|
||||
#include <scene/vk_scene.h>
|
||||
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
ChaseCameraMode::ChaseCameraMode(ChaseCameraSettings &settings)
|
||||
: _settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
void ChaseCameraMode::on_activate(SceneManager &scene, Camera &camera)
|
||||
{
|
||||
// If no target set, chase a point in front of the camera.
|
||||
if (_settings.target.type == CameraTargetType::None)
|
||||
{
|
||||
glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3(0.0f, 0.0f, -1.0f));
|
||||
_settings.target.type = CameraTargetType::WorldPoint;
|
||||
_settings.target.world_point = camera.position_world + WorldVec3(forward) * 10.0;
|
||||
}
|
||||
|
||||
// Preserve current relative transform to the target when possible.
|
||||
WorldVec3 target_pos{};
|
||||
glm::quat target_rot{};
|
||||
if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
glm::quat inv_target = glm::inverse(target_rot);
|
||||
glm::vec3 rel_pos = glm::vec3(camera.position_world - target_pos);
|
||||
_settings.position_offset_local = glm::rotate(inv_target, rel_pos);
|
||||
_settings.rotation_offset = glm::normalize(inv_target * camera.orientation);
|
||||
}
|
||||
|
||||
void ChaseCameraMode::process_input(SceneManager & /*scene*/,
|
||||
Camera & /*camera*/,
|
||||
InputSystem & /*input*/,
|
||||
bool /*ui_capture_keyboard*/,
|
||||
bool /*ui_capture_mouse*/)
|
||||
{
|
||||
}
|
||||
|
||||
void ChaseCameraMode::update(SceneManager &scene, Camera &camera, float dt)
|
||||
{
|
||||
WorldVec3 target_pos{};
|
||||
glm::quat target_rot{};
|
||||
if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 offset_world = glm::rotate(target_rot, _settings.position_offset_local);
|
||||
WorldVec3 desired_pos = target_pos + WorldVec3(offset_world);
|
||||
glm::quat desired_rot = glm::normalize(target_rot * _settings.rotation_offset);
|
||||
|
||||
if (dt > 0.0f)
|
||||
{
|
||||
float pos_alpha = 1.0f - std::exp(-_settings.position_lag * dt);
|
||||
float rot_alpha = 1.0f - std::exp(-_settings.rotation_lag * dt);
|
||||
|
||||
camera.position_world += (desired_pos - camera.position_world) * static_cast<double>(pos_alpha);
|
||||
camera.orientation = glm::normalize(glm::slerp(camera.orientation, desired_rot, rot_alpha));
|
||||
}
|
||||
else
|
||||
{
|
||||
camera.position_world = desired_pos;
|
||||
camera.orientation = desired_rot;
|
||||
}
|
||||
}
|
||||
24
src/scene/camera/mode_chase.h
Normal file
24
src/scene/camera/mode_chase.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <scene/camera/icamera_mode.h>
|
||||
|
||||
struct ChaseCameraSettings;
|
||||
|
||||
class ChaseCameraMode : public ICameraMode
|
||||
{
|
||||
public:
|
||||
explicit ChaseCameraMode(ChaseCameraSettings &settings);
|
||||
~ChaseCameraMode() override = default;
|
||||
|
||||
const char *name() const override { return "Chase"; }
|
||||
void on_activate(SceneManager &scene, Camera &camera) override;
|
||||
void process_input(SceneManager &scene,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse) override;
|
||||
void update(SceneManager &scene, Camera &camera, float dt) override;
|
||||
|
||||
private:
|
||||
ChaseCameraSettings &_settings;
|
||||
};
|
||||
25
src/scene/camera/mode_fixed.cpp
Normal file
25
src/scene/camera/mode_fixed.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
#include "mode_fixed.h"
|
||||
|
||||
#include <scene/camera/camera_rig.h>
|
||||
|
||||
FixedCameraMode::FixedCameraMode(FixedCameraSettings &settings)
|
||||
: _settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
void FixedCameraMode::on_activate(SceneManager & /*scene*/, Camera & /*camera*/)
|
||||
{
|
||||
}
|
||||
|
||||
void FixedCameraMode::process_input(SceneManager & /*scene*/,
|
||||
Camera & /*camera*/,
|
||||
InputSystem & /*input*/,
|
||||
bool /*ui_capture_keyboard*/,
|
||||
bool /*ui_capture_mouse*/)
|
||||
{
|
||||
}
|
||||
|
||||
void FixedCameraMode::update(SceneManager & /*scene*/, Camera & /*camera*/, float /*dt*/)
|
||||
{
|
||||
(void)_settings;
|
||||
}
|
||||
24
src/scene/camera/mode_fixed.h
Normal file
24
src/scene/camera/mode_fixed.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <scene/camera/icamera_mode.h>
|
||||
|
||||
struct FixedCameraSettings;
|
||||
|
||||
class FixedCameraMode : public ICameraMode
|
||||
{
|
||||
public:
|
||||
explicit FixedCameraMode(FixedCameraSettings &settings);
|
||||
~FixedCameraMode() override = default;
|
||||
|
||||
const char *name() const override { return "Fixed"; }
|
||||
void on_activate(SceneManager &scene, Camera &camera) override;
|
||||
void process_input(SceneManager &scene,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse) override;
|
||||
void update(SceneManager &scene, Camera &camera, float dt) override;
|
||||
|
||||
private:
|
||||
FixedCameraSettings &_settings;
|
||||
};
|
||||
58
src/scene/camera/mode_follow.cpp
Normal file
58
src/scene/camera/mode_follow.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "mode_follow.h"
|
||||
|
||||
#include <scene/camera/camera_rig.h>
|
||||
#include <scene/camera.h>
|
||||
#include <scene/vk_scene.h>
|
||||
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
FollowCameraMode::FollowCameraMode(FollowCameraSettings &settings)
|
||||
: _settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
void FollowCameraMode::on_activate(SceneManager &scene, Camera &camera)
|
||||
{
|
||||
// If no target set, follow a point in front of the camera.
|
||||
if (_settings.target.type == CameraTargetType::None)
|
||||
{
|
||||
glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3(0.0f, 0.0f, -1.0f));
|
||||
_settings.target.type = CameraTargetType::WorldPoint;
|
||||
_settings.target.world_point = camera.position_world + WorldVec3(forward) * 10.0;
|
||||
}
|
||||
|
||||
// Preserve current relative transform to the target when possible.
|
||||
WorldVec3 target_pos{};
|
||||
glm::quat target_rot{};
|
||||
if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
glm::quat inv_target = glm::inverse(target_rot);
|
||||
glm::vec3 rel_pos = glm::vec3(camera.position_world - target_pos);
|
||||
_settings.position_offset_local = glm::rotate(inv_target, rel_pos);
|
||||
_settings.rotation_offset = glm::normalize(inv_target * camera.orientation);
|
||||
}
|
||||
|
||||
void FollowCameraMode::process_input(SceneManager & /*scene*/,
|
||||
Camera & /*camera*/,
|
||||
InputSystem & /*input*/,
|
||||
bool /*ui_capture_keyboard*/,
|
||||
bool /*ui_capture_mouse*/)
|
||||
{
|
||||
}
|
||||
|
||||
void FollowCameraMode::update(SceneManager &scene, Camera &camera, float /*dt*/)
|
||||
{
|
||||
WorldVec3 target_pos{};
|
||||
glm::quat target_rot{};
|
||||
if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
glm::vec3 offset_world = glm::rotate(target_rot, _settings.position_offset_local);
|
||||
camera.position_world = target_pos + WorldVec3(offset_world);
|
||||
camera.orientation = glm::normalize(target_rot * _settings.rotation_offset);
|
||||
}
|
||||
24
src/scene/camera/mode_follow.h
Normal file
24
src/scene/camera/mode_follow.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <scene/camera/icamera_mode.h>
|
||||
|
||||
struct FollowCameraSettings;
|
||||
|
||||
class FollowCameraMode : public ICameraMode
|
||||
{
|
||||
public:
|
||||
explicit FollowCameraMode(FollowCameraSettings &settings);
|
||||
~FollowCameraMode() override = default;
|
||||
|
||||
const char *name() const override { return "Follow"; }
|
||||
void on_activate(SceneManager &scene, Camera &camera) override;
|
||||
void process_input(SceneManager &scene,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse) override;
|
||||
void update(SceneManager &scene, Camera &camera, float dt) override;
|
||||
|
||||
private:
|
||||
FollowCameraSettings &_settings;
|
||||
};
|
||||
147
src/scene/camera/mode_free.cpp
Normal file
147
src/scene/camera/mode_free.cpp
Normal file
@@ -0,0 +1,147 @@
|
||||
#include "mode_free.h"
|
||||
|
||||
#include <scene/camera/camera_rig.h>
|
||||
#include <scene/camera.h>
|
||||
|
||||
#include <core/input/input_system.h>
|
||||
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
FreeCameraMode::FreeCameraMode(FreeCameraSettings &settings)
|
||||
: _settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
void FreeCameraMode::on_activate(SceneManager & /*scene*/, Camera & /*camera*/)
|
||||
{
|
||||
_velocity = glm::vec3(0.0f);
|
||||
_roll_dir = 0.0f;
|
||||
_rmb_down = false;
|
||||
}
|
||||
|
||||
void FreeCameraMode::process_input(SceneManager & /*scene*/,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse)
|
||||
{
|
||||
const InputState &st = input.state();
|
||||
|
||||
// Movement is state-based so simultaneous keys work naturally.
|
||||
if (ui_capture_keyboard)
|
||||
{
|
||||
_velocity = glm::vec3(0.0f);
|
||||
_roll_dir = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
glm::vec3 v(0.0f);
|
||||
if (st.key_down(Key::W)) { v.z -= 1.0f; }
|
||||
if (st.key_down(Key::S)) { v.z += 1.0f; }
|
||||
if (st.key_down(Key::A)) { v.x -= 1.0f; }
|
||||
if (st.key_down(Key::D)) { v.x += 1.0f; }
|
||||
_velocity = v;
|
||||
|
||||
float roll = 0.0f;
|
||||
if (st.key_down(Key::Q)) { roll -= 1.0f; }
|
||||
if (st.key_down(Key::E)) { roll += 1.0f; }
|
||||
_roll_dir = roll;
|
||||
}
|
||||
|
||||
// Event-based mouse handling so we don't apply motion that happened
|
||||
// before RMB was pressed in the same frame.
|
||||
for (const InputEvent &e : input.events())
|
||||
{
|
||||
if (ui_capture_mouse)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type == InputEvent::Type::MouseButtonDown && e.mouse_button == MouseButton::Right)
|
||||
{
|
||||
_rmb_down = true;
|
||||
input.set_cursor_mode(CursorMode::Relative);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseButtonUp && e.mouse_button == MouseButton::Right)
|
||||
{
|
||||
_rmb_down = false;
|
||||
input.set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseMove && _rmb_down)
|
||||
{
|
||||
// Convert mouse motion to incremental yaw/pitch angles.
|
||||
float dx = e.mouse_delta.x * _settings.look_sensitivity;
|
||||
float dy = e.mouse_delta.y * _settings.look_sensitivity;
|
||||
|
||||
// Mouse right (xrel > 0) turns view right with -Z-forward: yaw around +Y.
|
||||
glm::quat yaw_rotation = glm::angleAxis(dx, glm::vec3{0.f, 1.f, 0.f});
|
||||
|
||||
// Mouse up (yrel < 0) looks up with -Z-forward: negative dy.
|
||||
float pitch_delta = -dy;
|
||||
// Pitch around the camera's local X (right) axis in world space.
|
||||
glm::vec3 right = glm::rotate(camera.orientation, glm::vec3{1.f, 0.f, 0.f});
|
||||
glm::quat pitch_rotation = glm::angleAxis(pitch_delta, glm::vec3(right));
|
||||
|
||||
// Apply yaw, then pitch, to the current orientation.
|
||||
camera.orientation = glm::normalize(pitch_rotation * yaw_rotation * camera.orientation);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseWheel)
|
||||
{
|
||||
const float steps = e.wheel_delta.y; // positive = wheel up
|
||||
if (std::abs(steps) < 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ctrl modifies FOV, otherwise adjust move speed.
|
||||
if (e.mods.ctrl)
|
||||
{
|
||||
// Wheel up -> zoom in (smaller FOV)
|
||||
camera.fovDegrees -= steps * 2.0f;
|
||||
camera.fovDegrees = std::clamp(camera.fovDegrees, 30.0f, 110.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Exponential scale for pleasant feel
|
||||
float factor = std::pow(1.15f, steps);
|
||||
_settings.move_speed = std::clamp(_settings.move_speed * factor, 0.06f, 300.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: if mouse state shows RMB is no longer down, release relative mode.
|
||||
if (_rmb_down && !st.mouse_down(MouseButton::Right))
|
||||
{
|
||||
_rmb_down = false;
|
||||
input.set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
void FreeCameraMode::update(SceneManager & /*scene*/, Camera &camera, float dt)
|
||||
{
|
||||
if (dt <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Roll around the camera's forward axis (world-space axis).
|
||||
if (_roll_dir != 0.0f && _settings.roll_speed > 0.0f)
|
||||
{
|
||||
glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3{0.0f, 0.0f, -1.0f});
|
||||
float angle = _roll_dir * _settings.roll_speed * dt;
|
||||
glm::quat roll_rotation = glm::angleAxis(angle, glm::normalize(forward));
|
||||
camera.orientation = glm::normalize(roll_rotation * camera.orientation);
|
||||
}
|
||||
|
||||
// Move in camera-local space.
|
||||
if (_velocity.x != 0.0f || _velocity.y != 0.0f || _velocity.z != 0.0f)
|
||||
{
|
||||
glm::vec3 local_delta = _velocity * (_settings.move_speed * dt);
|
||||
glm::mat4 camera_rotation = camera.getRotationMatrix();
|
||||
glm::vec3 world_delta = glm::vec3(camera_rotation * glm::vec4(local_delta, 0.0f));
|
||||
camera.position_world += glm::dvec3(world_delta);
|
||||
}
|
||||
}
|
||||
27
src/scene/camera/mode_free.h
Normal file
27
src/scene/camera/mode_free.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <scene/camera/icamera_mode.h>
|
||||
|
||||
struct FreeCameraSettings;
|
||||
|
||||
class FreeCameraMode : public ICameraMode
|
||||
{
|
||||
public:
|
||||
explicit FreeCameraMode(FreeCameraSettings &settings);
|
||||
~FreeCameraMode() override = default;
|
||||
|
||||
const char *name() const override { return "Free"; }
|
||||
void on_activate(SceneManager &scene, Camera &camera) override;
|
||||
void process_input(SceneManager &scene,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse) override;
|
||||
void update(SceneManager &scene, Camera &camera, float dt) override;
|
||||
|
||||
private:
|
||||
FreeCameraSettings &_settings;
|
||||
glm::vec3 _velocity{0.0f, 0.0f, 0.0f};
|
||||
float _roll_dir = 0.0f;
|
||||
bool _rmb_down = false;
|
||||
};
|
||||
163
src/scene/camera/mode_orbit.cpp
Normal file
163
src/scene/camera/mode_orbit.cpp
Normal file
@@ -0,0 +1,163 @@
|
||||
#include "mode_orbit.h"
|
||||
|
||||
#include <scene/camera/camera_rig.h>
|
||||
#include <scene/camera.h>
|
||||
#include <scene/vk_scene.h>
|
||||
|
||||
#include <core/input/input_system.h>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
#include <glm/gtx/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
OrbitCameraMode::OrbitCameraMode(OrbitCameraSettings &settings)
|
||||
: _settings(settings)
|
||||
{
|
||||
}
|
||||
|
||||
void OrbitCameraMode::on_activate(SceneManager &scene, Camera &camera)
|
||||
{
|
||||
_rmb_down = false;
|
||||
|
||||
// If no target set, orbit around a point in front of the camera.
|
||||
if (_settings.target.type == CameraTargetType::None)
|
||||
{
|
||||
glm::vec3 forward = glm::rotate(camera.orientation, glm::vec3(0.0f, 0.0f, -1.0f));
|
||||
_settings.target.type = CameraTargetType::WorldPoint;
|
||||
_settings.target.world_point = camera.position_world + WorldVec3(forward) * _settings.distance;
|
||||
}
|
||||
|
||||
// Derive yaw/pitch/distance from current camera pose to avoid snapping.
|
||||
WorldVec3 target_pos{};
|
||||
glm::quat target_rot{};
|
||||
if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WorldVec3 to_cam = camera.position_world - target_pos;
|
||||
double dist = glm::length(to_cam);
|
||||
if (!std::isfinite(dist) || dist < 0.001)
|
||||
{
|
||||
dist = _settings.distance;
|
||||
}
|
||||
_settings.distance = dist;
|
||||
|
||||
glm::vec3 dir = glm::normalize(glm::vec3(to_cam / dist)); // target -> camera
|
||||
_settings.yaw = std::atan2(dir.x, dir.z);
|
||||
_settings.pitch = std::asin(std::clamp(-dir.y, -1.0f, 1.0f));
|
||||
}
|
||||
|
||||
void OrbitCameraMode::process_input(SceneManager & /*scene*/,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool /*ui_capture_keyboard*/,
|
||||
bool ui_capture_mouse)
|
||||
{
|
||||
const InputState &st = input.state();
|
||||
|
||||
for (const InputEvent &e : input.events())
|
||||
{
|
||||
if (ui_capture_mouse)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type == InputEvent::Type::MouseButtonDown && e.mouse_button == MouseButton::Right)
|
||||
{
|
||||
_rmb_down = true;
|
||||
input.set_cursor_mode(CursorMode::Relative);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseButtonUp && e.mouse_button == MouseButton::Right)
|
||||
{
|
||||
_rmb_down = false;
|
||||
input.set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseMove && _rmb_down)
|
||||
{
|
||||
float dx = e.mouse_delta.x * _settings.look_sensitivity;
|
||||
float dy = e.mouse_delta.y * _settings.look_sensitivity;
|
||||
|
||||
_settings.yaw += dx;
|
||||
_settings.pitch += dy;
|
||||
|
||||
const float limit = glm::half_pi<float>() - 0.01f;
|
||||
_settings.pitch = std::clamp(_settings.pitch, -limit, limit);
|
||||
}
|
||||
else if (e.type == InputEvent::Type::MouseWheel)
|
||||
{
|
||||
const float steps = e.wheel_delta.y; // positive = wheel up
|
||||
if (std::abs(steps) < 0.001f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.mods.ctrl)
|
||||
{
|
||||
camera.fovDegrees -= steps * 2.0f;
|
||||
camera.fovDegrees = std::clamp(camera.fovDegrees, 30.0f, 110.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
const double factor = std::pow(1.15, -static_cast<double>(steps));
|
||||
_settings.distance = std::clamp(_settings.distance * factor, 0.2, 100000.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_rmb_down && !st.mouse_down(MouseButton::Right))
|
||||
{
|
||||
_rmb_down = false;
|
||||
input.set_cursor_mode(CursorMode::Normal);
|
||||
}
|
||||
}
|
||||
|
||||
void OrbitCameraMode::update(SceneManager &scene, Camera &camera, float /*dt*/)
|
||||
{
|
||||
WorldVec3 target_pos{};
|
||||
glm::quat target_rot{};
|
||||
if (!scene.getCameraRig().resolve_target(scene, _settings.target, target_pos, target_rot))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto wrap_pi = [](float a) -> float {
|
||||
// Wrap angle to [-pi, pi] to avoid precision issues over long play sessions.
|
||||
constexpr float two_pi = glm::two_pi<float>();
|
||||
a = std::fmod(a + glm::pi<float>(), two_pi);
|
||||
if (a < 0.0f) a += two_pi;
|
||||
return a - glm::pi<float>();
|
||||
};
|
||||
|
||||
float yaw = wrap_pi(_settings.yaw);
|
||||
float pitch = std::clamp(_settings.pitch,
|
||||
-glm::half_pi<float>() + 0.01f,
|
||||
glm::half_pi<float>() - 0.01f);
|
||||
_settings.yaw = yaw;
|
||||
_settings.pitch = pitch;
|
||||
double dist = std::max(0.2, _settings.distance);
|
||||
|
||||
glm::quat yaw_q = glm::angleAxis(yaw, glm::vec3(0.0f, 1.0f, 0.0f));
|
||||
glm::vec3 right = glm::rotate(yaw_q, glm::vec3(1.0f, 0.0f, 0.0f));
|
||||
glm::quat pitch_q = glm::angleAxis(pitch, right);
|
||||
glm::quat orbit_q = glm::normalize(pitch_q * yaw_q);
|
||||
|
||||
// Place the camera on its local +Z axis relative to the target so the camera's
|
||||
// -Z forward axis points toward the target.
|
||||
const double yaw_d = static_cast<double>(yaw);
|
||||
const double pitch_d = static_cast<double>(pitch);
|
||||
const double cos_pitch = std::cos(pitch_d);
|
||||
const double sin_pitch = std::sin(pitch_d);
|
||||
const double sin_yaw = std::sin(yaw_d);
|
||||
const double cos_yaw = std::cos(yaw_d);
|
||||
|
||||
const glm::dvec3 dir_target_to_camera(
|
||||
sin_yaw * cos_pitch,
|
||||
-sin_pitch,
|
||||
cos_yaw * cos_pitch);
|
||||
|
||||
camera.position_world = target_pos + dir_target_to_camera * dist;
|
||||
camera.orientation = orbit_q;
|
||||
}
|
||||
25
src/scene/camera/mode_orbit.h
Normal file
25
src/scene/camera/mode_orbit.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <scene/camera/icamera_mode.h>
|
||||
|
||||
struct OrbitCameraSettings;
|
||||
|
||||
class OrbitCameraMode : public ICameraMode
|
||||
{
|
||||
public:
|
||||
explicit OrbitCameraMode(OrbitCameraSettings &settings);
|
||||
~OrbitCameraMode() override = default;
|
||||
|
||||
const char *name() const override { return "Orbit"; }
|
||||
void on_activate(SceneManager &scene, Camera &camera) override;
|
||||
void process_input(SceneManager &scene,
|
||||
Camera &camera,
|
||||
InputSystem &input,
|
||||
bool ui_capture_keyboard,
|
||||
bool ui_capture_mouse) override;
|
||||
void update(SceneManager &scene, Camera &camera, float dt) override;
|
||||
|
||||
private:
|
||||
OrbitCameraSettings &_settings;
|
||||
bool _rmb_down = false;
|
||||
};
|
||||
314
src/scene/planet/cubesphere.cpp
Normal file
314
src/scene/planet/cubesphere.cpp
Normal file
@@ -0,0 +1,314 @@
|
||||
#include "cubesphere.h"
|
||||
|
||||
#include <scene/tangent_space.h>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
namespace planet
|
||||
{
|
||||
glm::dvec3 cubesphere_unit_direction(CubeFace face, double u, double v)
|
||||
{
|
||||
// Convention: u increases right, v increases down (image space).
|
||||
glm::dvec3 d(0.0);
|
||||
switch (face)
|
||||
{
|
||||
case CubeFace::PosX: d = glm::dvec3(1.0, -v, -u); break;
|
||||
case CubeFace::NegX: d = glm::dvec3(-1.0, -v, u); break;
|
||||
case CubeFace::PosY: d = glm::dvec3(u, 1.0, v); break;
|
||||
case CubeFace::NegY: d = glm::dvec3(u, -1.0, -v); break;
|
||||
case CubeFace::PosZ: d = glm::dvec3(u, -v, 1.0); break;
|
||||
case CubeFace::NegZ: d = glm::dvec3(-u, -v, -1.0); break;
|
||||
}
|
||||
|
||||
const double len2 = glm::dot(d, d);
|
||||
if (len2 <= 0.0)
|
||||
{
|
||||
return glm::dvec3(0.0, 0.0, 1.0);
|
||||
}
|
||||
return d * (1.0 / std::sqrt(len2));
|
||||
}
|
||||
|
||||
void cubesphere_tile_uv_bounds(uint32_t level, uint32_t x, uint32_t y,
|
||||
double &out_u0, double &out_u1,
|
||||
double &out_v0, double &out_v1)
|
||||
{
|
||||
const uint32_t tiles_u = (level < 31u) ? (1u << level) : 0u;
|
||||
const double inv_tiles = (tiles_u > 0u) ? (1.0 / static_cast<double>(tiles_u)) : 1.0;
|
||||
|
||||
const double u0_01 = static_cast<double>(x) * inv_tiles;
|
||||
const double u1_01 = static_cast<double>(x + 1u) * inv_tiles;
|
||||
const double v0_01 = static_cast<double>(y) * inv_tiles;
|
||||
const double v1_01 = static_cast<double>(y + 1u) * inv_tiles;
|
||||
|
||||
out_u0 = u0_01 * 2.0 - 1.0;
|
||||
out_u1 = u1_01 * 2.0 - 1.0;
|
||||
out_v0 = v0_01 * 2.0 - 1.0;
|
||||
out_v1 = v1_01 * 2.0 - 1.0;
|
||||
}
|
||||
|
||||
glm::dvec3 cubesphere_patch_center_direction(CubeFace face, uint32_t level, uint32_t x, uint32_t y)
|
||||
{
|
||||
double u0 = 0.0, u1 = 0.0, v0 = 0.0, v1 = 0.0;
|
||||
cubesphere_tile_uv_bounds(level, x, y, u0, u1, v0, v1);
|
||||
const double u_mid = 0.5 * (u0 + u1);
|
||||
const double v_mid = 0.5 * (v0 + v1);
|
||||
return cubesphere_unit_direction(face, u_mid, v_mid);
|
||||
}
|
||||
|
||||
WorldVec3 cubesphere_patch_center_world(const WorldVec3 ¢er_world,
|
||||
double radius_m,
|
||||
CubeFace face,
|
||||
uint32_t level,
|
||||
uint32_t x,
|
||||
uint32_t y)
|
||||
{
|
||||
const glm::dvec3 dir = cubesphere_patch_center_direction(face, level, x, y);
|
||||
return center_world + dir * radius_m;
|
||||
}
|
||||
|
||||
double cubesphere_patch_edge_m(double radius_m, uint32_t level)
|
||||
{
|
||||
// Each cube face spans 90 degrees. Use arc length per tile edge as a simple estimate.
|
||||
const double face_arc_m = (glm::pi<double>() * 0.5) * radius_m;
|
||||
const uint32_t safe_level = (level < 30u) ? level : 30u;
|
||||
const double tiles_per_axis = static_cast<double>(1u << safe_level);
|
||||
return face_arc_m / tiles_per_axis;
|
||||
}
|
||||
|
||||
double cubesphere_skirt_depth_m(double radius_m, uint32_t level)
|
||||
{
|
||||
const double edge_m = cubesphere_patch_edge_m(radius_m, level);
|
||||
return glm::max(10.0, 0.02 * edge_m);
|
||||
}
|
||||
|
||||
void build_cubesphere_patch_indices(std::vector<uint32_t> &out_indices, uint32_t resolution)
|
||||
{
|
||||
out_indices.clear();
|
||||
|
||||
if (resolution < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t grid_index_count =
|
||||
static_cast<size_t>(resolution - 1u) * static_cast<size_t>(resolution - 1u) * 6u;
|
||||
const size_t skirt_index_count = static_cast<size_t>(4u) * static_cast<size_t>(resolution - 1u) * 6u;
|
||||
out_indices.reserve(grid_index_count + skirt_index_count);
|
||||
|
||||
// Base grid indices
|
||||
for (uint32_t j = 0; j + 1 < resolution; ++j)
|
||||
{
|
||||
for (uint32_t i = 0; i + 1 < resolution; ++i)
|
||||
{
|
||||
const uint32_t i0 = j * resolution + i;
|
||||
const uint32_t i1 = i0 + 1;
|
||||
const uint32_t i2 = i0 + resolution;
|
||||
const uint32_t i3 = i2 + 1;
|
||||
|
||||
// CCW winding when viewed from outside the sphere.
|
||||
out_indices.push_back(i0);
|
||||
out_indices.push_back(i1);
|
||||
out_indices.push_back(i2);
|
||||
|
||||
out_indices.push_back(i2);
|
||||
out_indices.push_back(i1);
|
||||
out_indices.push_back(i3);
|
||||
}
|
||||
}
|
||||
|
||||
auto add_skirt_quads = [&](uint32_t base0, uint32_t base1, uint32_t skirt0, uint32_t skirt1)
|
||||
{
|
||||
out_indices.push_back(base0);
|
||||
out_indices.push_back(base1);
|
||||
out_indices.push_back(skirt0);
|
||||
|
||||
out_indices.push_back(skirt0);
|
||||
out_indices.push_back(base1);
|
||||
out_indices.push_back(skirt1);
|
||||
};
|
||||
|
||||
const uint32_t base_vertex_count = resolution * resolution;
|
||||
const uint32_t top_skirt_start = base_vertex_count + 0u * resolution;
|
||||
const uint32_t right_skirt_start = base_vertex_count + 1u * resolution;
|
||||
const uint32_t bottom_skirt_start = base_vertex_count + 2u * resolution;
|
||||
const uint32_t left_skirt_start = base_vertex_count + 3u * resolution;
|
||||
|
||||
// Skirt indices: 4 edges, (N-1) segments each.
|
||||
for (uint32_t i = 0; i + 1 < resolution; ++i)
|
||||
{
|
||||
// Top edge
|
||||
add_skirt_quads(0u * resolution + i,
|
||||
0u * resolution + (i + 1u),
|
||||
top_skirt_start + i,
|
||||
top_skirt_start + (i + 1u));
|
||||
// Bottom edge
|
||||
add_skirt_quads((resolution - 1u) * resolution + i,
|
||||
(resolution - 1u) * resolution + (i + 1u),
|
||||
bottom_skirt_start + i,
|
||||
bottom_skirt_start + (i + 1u));
|
||||
}
|
||||
for (uint32_t j = 0; j + 1 < resolution; ++j)
|
||||
{
|
||||
// Left edge
|
||||
add_skirt_quads(j * resolution + 0u,
|
||||
(j + 1u) * resolution + 0u,
|
||||
left_skirt_start + j,
|
||||
left_skirt_start + (j + 1u));
|
||||
// Right edge
|
||||
add_skirt_quads(j * resolution + (resolution - 1u),
|
||||
(j + 1u) * resolution + (resolution - 1u),
|
||||
right_skirt_start + j,
|
||||
right_skirt_start + (j + 1u));
|
||||
}
|
||||
}
|
||||
|
||||
glm::dvec3 build_cubesphere_patch_vertices(std::vector<Vertex> &out_vertices,
|
||||
double radius_m,
|
||||
CubeFace face,
|
||||
uint32_t level,
|
||||
uint32_t x,
|
||||
uint32_t y,
|
||||
uint32_t resolution,
|
||||
const glm::vec4 &vertex_color)
|
||||
{
|
||||
out_vertices.clear();
|
||||
|
||||
if (resolution < 2)
|
||||
{
|
||||
return glm::dvec3(0.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
const glm::dvec3 patch_center_dir = cubesphere_patch_center_direction(face, level, x, y);
|
||||
|
||||
const double skirt_depth_m = cubesphere_skirt_depth_m(radius_m, level);
|
||||
const double skirt_radius_m = glm::max(0.0, radius_m - skirt_depth_m);
|
||||
|
||||
double u0 = 0.0, u1 = 0.0, v0 = 0.0, v1 = 0.0;
|
||||
cubesphere_tile_uv_bounds(level, x, y, u0, u1, v0, v1);
|
||||
|
||||
const uint32_t base_vertex_count = resolution * resolution;
|
||||
const uint32_t skirt_vertex_count = 4u * resolution;
|
||||
out_vertices.resize(static_cast<size_t>(base_vertex_count) + static_cast<size_t>(skirt_vertex_count));
|
||||
|
||||
const uint32_t tiles_per_axis = (level < 31u) ? (1u << level) : 1u;
|
||||
const double inv_tiles = (tiles_per_axis > 0u) ? (1.0 / static_cast<double>(tiles_per_axis)) : 1.0;
|
||||
|
||||
const double inv = 1.0 / static_cast<double>(resolution - 1u);
|
||||
const double du = (u1 - u0) * inv;
|
||||
const double dv = (v1 - v0) * inv;
|
||||
for (uint32_t j = 0; j < resolution; ++j)
|
||||
{
|
||||
const float t = static_cast<float>(static_cast<double>(j) * inv);
|
||||
const double v = v0 + dv * static_cast<double>(j);
|
||||
|
||||
for (uint32_t i = 0; i < resolution; ++i)
|
||||
{
|
||||
const float s = static_cast<float>(static_cast<double>(i) * inv);
|
||||
const double u = u0 + du * static_cast<double>(i);
|
||||
|
||||
const glm::dvec3 unit_dir = cubesphere_unit_direction(face, u, v);
|
||||
const glm::dvec3 delta_d = (unit_dir - patch_center_dir) * radius_m;
|
||||
|
||||
Vertex vert{};
|
||||
vert.position = glm::vec3(static_cast<float>(delta_d.x),
|
||||
static_cast<float>(delta_d.y),
|
||||
static_cast<float>(delta_d.z));
|
||||
vert.normal = glm::vec3(static_cast<float>(unit_dir.x),
|
||||
static_cast<float>(unit_dir.y),
|
||||
static_cast<float>(unit_dir.z));
|
||||
|
||||
// UVs cover the entire cube face (0..1) so all patches on this face
|
||||
// sample from a single per-face texture.
|
||||
const double u_face = (static_cast<double>(x) + static_cast<double>(s)) * inv_tiles;
|
||||
const double v_face = (static_cast<double>(y) + static_cast<double>(t)) * inv_tiles;
|
||||
vert.uv_x = static_cast<float>(u_face);
|
||||
vert.uv_y = static_cast<float>(v_face);
|
||||
vert.color = vertex_color;
|
||||
vert.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f);
|
||||
|
||||
const uint32_t idx = j * resolution + i;
|
||||
out_vertices[idx] = vert;
|
||||
}
|
||||
}
|
||||
|
||||
auto add_skirt_vertex = [&](uint32_t base_index, uint32_t skirt_index)
|
||||
{
|
||||
const glm::vec3 n = out_vertices[base_index].normal;
|
||||
const glm::dvec3 unit_dir(static_cast<double>(n.x),
|
||||
static_cast<double>(n.y),
|
||||
static_cast<double>(n.z));
|
||||
const glm::dvec3 delta_d = unit_dir * skirt_radius_m - patch_center_dir * radius_m;
|
||||
|
||||
Vertex vert = out_vertices[base_index];
|
||||
vert.position = glm::vec3(static_cast<float>(delta_d.x),
|
||||
static_cast<float>(delta_d.y),
|
||||
static_cast<float>(delta_d.z));
|
||||
vert.normal = glm::vec3(static_cast<float>(unit_dir.x),
|
||||
static_cast<float>(unit_dir.y),
|
||||
static_cast<float>(unit_dir.z));
|
||||
out_vertices[skirt_index] = vert;
|
||||
};
|
||||
|
||||
const uint32_t top_skirt_start = base_vertex_count + 0u * resolution;
|
||||
const uint32_t right_skirt_start = base_vertex_count + 1u * resolution;
|
||||
const uint32_t bottom_skirt_start = base_vertex_count + 2u * resolution;
|
||||
const uint32_t left_skirt_start = base_vertex_count + 3u * resolution;
|
||||
|
||||
// Top edge (j=0)
|
||||
for (uint32_t i = 0; i < resolution; ++i)
|
||||
{
|
||||
add_skirt_vertex(0u * resolution + i, top_skirt_start + i);
|
||||
}
|
||||
// Right edge (i=resolution-1)
|
||||
for (uint32_t j = 0; j < resolution; ++j)
|
||||
{
|
||||
add_skirt_vertex(j * resolution + (resolution - 1u), right_skirt_start + j);
|
||||
}
|
||||
// Bottom edge (j=resolution-1)
|
||||
for (uint32_t i = 0; i < resolution; ++i)
|
||||
{
|
||||
add_skirt_vertex((resolution - 1u) * resolution + i, bottom_skirt_start + i);
|
||||
}
|
||||
// Left edge (i=0)
|
||||
for (uint32_t j = 0; j < resolution; ++j)
|
||||
{
|
||||
add_skirt_vertex(j * resolution + 0u, left_skirt_start + j);
|
||||
}
|
||||
|
||||
return patch_center_dir;
|
||||
}
|
||||
|
||||
void build_cubesphere_patch_mesh(CubeSpherePatchMesh &out,
|
||||
const WorldVec3 ¢er_world,
|
||||
double radius_m,
|
||||
CubeFace face,
|
||||
uint32_t level,
|
||||
uint32_t x,
|
||||
uint32_t y,
|
||||
uint32_t resolution,
|
||||
const glm::vec4 &vertex_color,
|
||||
bool generate_tangents)
|
||||
{
|
||||
out.vertices.clear();
|
||||
out.indices.clear();
|
||||
out.patch_center_world = center_world;
|
||||
|
||||
if (resolution < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const glm::dvec3 patch_center_dir =
|
||||
build_cubesphere_patch_vertices(out.vertices, radius_m, face, level, x, y, resolution, vertex_color);
|
||||
build_cubesphere_patch_indices(out.indices, resolution);
|
||||
|
||||
out.patch_center_world = center_world + patch_center_dir * radius_m;
|
||||
|
||||
if (generate_tangents)
|
||||
{
|
||||
geom::generate_tangents(out.vertices, out.indices);
|
||||
}
|
||||
}
|
||||
} // namespace planet
|
||||
84
src/scene/planet/cubesphere.h
Normal file
84
src/scene/planet/cubesphere.h
Normal file
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/types.h>
|
||||
#include <core/world.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include <glm/vec4.hpp>
|
||||
|
||||
namespace planet
|
||||
{
|
||||
// Cube face ordering matches KTX/Vulkan cubemap face order:
|
||||
// +X, -X, +Y, -Y, +Z, -Z
|
||||
enum class CubeFace : uint8_t
|
||||
{
|
||||
PosX = 0,
|
||||
NegX = 1,
|
||||
PosY = 2,
|
||||
NegY = 3,
|
||||
PosZ = 4,
|
||||
NegZ = 5,
|
||||
};
|
||||
|
||||
// u,v are in [-1,+1] on the chosen face. Convention:
|
||||
// - u increases to the right
|
||||
// - v increases downward (image space)
|
||||
glm::dvec3 cubesphere_unit_direction(CubeFace face, double u, double v);
|
||||
|
||||
// Tile bounds on a face in cube-face parametric space:
|
||||
// u,v in [-1,+1], where [0..1] maps to [-1..+1].
|
||||
void cubesphere_tile_uv_bounds(uint32_t level, uint32_t x, uint32_t y,
|
||||
double &out_u0, double &out_u1,
|
||||
double &out_v0, double &out_v1);
|
||||
|
||||
glm::dvec3 cubesphere_patch_center_direction(CubeFace face, uint32_t level, uint32_t x, uint32_t y);
|
||||
|
||||
WorldVec3 cubesphere_patch_center_world(const WorldVec3 ¢er_world,
|
||||
double radius_m,
|
||||
CubeFace face,
|
||||
uint32_t level,
|
||||
uint32_t x,
|
||||
uint32_t y);
|
||||
|
||||
// Approximate world-space tile edge length on the sphere surface.
|
||||
double cubesphere_patch_edge_m(double radius_m, uint32_t level);
|
||||
|
||||
// Skirt depth heuristic (meters).
|
||||
double cubesphere_skirt_depth_m(double radius_m, uint32_t level);
|
||||
|
||||
struct CubeSpherePatchMesh
|
||||
{
|
||||
std::vector<Vertex> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
WorldVec3 patch_center_world{0.0, 0.0, 0.0};
|
||||
};
|
||||
|
||||
// Build the shared index list for a patch grid with skirts. Indices are identical for all
|
||||
// patches as long as 'resolution' is constant.
|
||||
void build_cubesphere_patch_indices(std::vector<uint32_t> &out_indices, uint32_t resolution);
|
||||
|
||||
// Build patch vertices (including skirts). Vertex positions are relative to the patch center on
|
||||
// the sphere surface (computed from face/level/x/y). Returns the patch center direction.
|
||||
glm::dvec3 build_cubesphere_patch_vertices(std::vector<Vertex> &out_vertices,
|
||||
double radius_m,
|
||||
CubeFace face,
|
||||
uint32_t level,
|
||||
uint32_t x,
|
||||
uint32_t y,
|
||||
uint32_t resolution,
|
||||
const glm::vec4 &vertex_color);
|
||||
|
||||
// Build a cube-sphere patch mesh with skirts. Vertex positions are relative to patch_center_world.
|
||||
void build_cubesphere_patch_mesh(CubeSpherePatchMesh &out,
|
||||
const WorldVec3 ¢er_world,
|
||||
double radius_m,
|
||||
CubeFace face,
|
||||
uint32_t level,
|
||||
uint32_t x,
|
||||
uint32_t y,
|
||||
uint32_t resolution,
|
||||
const glm::vec4 &vertex_color,
|
||||
bool generate_tangents = true);
|
||||
} // namespace planet
|
||||
296
src/scene/planet/planet_quadtree.cpp
Normal file
296
src/scene/planet/planet_quadtree.cpp
Normal file
@@ -0,0 +1,296 @@
|
||||
#include "planet_quadtree.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include <glm/gtc/constants.hpp>
|
||||
|
||||
namespace planet
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct Node
|
||||
{
|
||||
PatchKey key{};
|
||||
};
|
||||
|
||||
void compute_patch_visibility_terms(const PatchKey &key,
|
||||
const glm::dvec3 &patch_center_dir,
|
||||
double radius_m,
|
||||
double &out_cos_patch_radius,
|
||||
double &out_sin_patch_radius,
|
||||
double &out_bound_radius_m)
|
||||
{
|
||||
glm::dvec3 c = patch_center_dir;
|
||||
const double c_len2 = glm::dot(c, c);
|
||||
if (!(c_len2 > 0.0))
|
||||
{
|
||||
c = glm::dvec3(0.0, 0.0, 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
c *= (1.0 / std::sqrt(c_len2));
|
||||
}
|
||||
|
||||
double u0 = 0.0, u1 = 0.0, v0 = 0.0, v1 = 0.0;
|
||||
cubesphere_tile_uv_bounds(key.level, key.x, key.y, u0, u1, v0, v1);
|
||||
|
||||
// Conservative angular radius: max angle from patch center direction to any corner direction.
|
||||
double min_dot = 1.0;
|
||||
min_dot = std::min(min_dot, glm::dot(c, cubesphere_unit_direction(key.face, u0, v0)));
|
||||
min_dot = std::min(min_dot, glm::dot(c, cubesphere_unit_direction(key.face, u1, v0)));
|
||||
min_dot = std::min(min_dot, glm::dot(c, cubesphere_unit_direction(key.face, u0, v1)));
|
||||
min_dot = std::min(min_dot, glm::dot(c, cubesphere_unit_direction(key.face, u1, v1)));
|
||||
|
||||
const double cos_a = glm::clamp(min_dot, -1.0, 1.0);
|
||||
const double sin_a = std::sqrt(glm::max(0.0, 1.0 - cos_a * cos_a));
|
||||
|
||||
// Vertex positions are built as (unit_dir - patch_center_dir) * radius (chord length).
|
||||
const double chord_r = radius_m * std::sqrt(glm::max(0.0, 2.0 - 2.0 * cos_a));
|
||||
|
||||
// Skirts extend inward; add a small safety margin so CPU culling stays conservative.
|
||||
const double skirt_depth = cubesphere_skirt_depth_m(radius_m, key.level);
|
||||
|
||||
out_cos_patch_radius = cos_a;
|
||||
out_sin_patch_radius = sin_a;
|
||||
out_bound_radius_m = glm::max(1.0, chord_r + skirt_depth);
|
||||
}
|
||||
|
||||
bool is_patch_visible_horizon(const WorldVec3 &body_center_world,
|
||||
double radius_m,
|
||||
const WorldVec3 &camera_world,
|
||||
const glm::dvec3 &patch_center_dir,
|
||||
double cos_patch_radius,
|
||||
double sin_patch_radius)
|
||||
{
|
||||
const glm::dvec3 w = camera_world - body_center_world;
|
||||
const double d = glm::length(w);
|
||||
if (d <= radius_m || d <= 0.0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
const glm::dvec3 w_dir = w / d;
|
||||
const double cos_theta = glm::dot(patch_center_dir, w_dir);
|
||||
|
||||
// Horizon angle: cos(theta_h) = R / d
|
||||
const double cos_h = glm::clamp(radius_m / d, 0.0, 1.0);
|
||||
const double sin_h = std::sqrt(glm::max(0.0, 1.0 - cos_h * cos_h));
|
||||
|
||||
// Visible if theta <= theta_h + ang:
|
||||
// cos(theta) >= cos(theta_h + ang)
|
||||
const double cos_limit = cos_h * cos_patch_radius - sin_h * sin_patch_radius;
|
||||
if (!std::isfinite(cos_theta) || !std::isfinite(cos_limit))
|
||||
{
|
||||
return true; // fail-safe: avoid catastrophic full culls
|
||||
}
|
||||
return cos_theta >= cos_limit;
|
||||
}
|
||||
|
||||
bool is_patch_visible_frustum(const glm::vec3 ¢er_local, float bound_radius_m, const glm::mat4 &viewproj)
|
||||
{
|
||||
if (!(bound_radius_m > 0.0f))
|
||||
{
|
||||
bound_radius_m = 1.0f;
|
||||
}
|
||||
|
||||
// Conservative AABB-in-clip test for a cube around the patch center.
|
||||
const std::array<glm::vec3, 8> corners{
|
||||
glm::vec3{+1, +1, +1}, glm::vec3{+1, +1, -1}, glm::vec3{+1, -1, +1}, glm::vec3{+1, -1, -1},
|
||||
glm::vec3{-1, +1, +1}, glm::vec3{-1, +1, -1}, glm::vec3{-1, -1, +1}, glm::vec3{-1, -1, -1},
|
||||
};
|
||||
|
||||
glm::vec4 clip[8];
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
const glm::vec3 p = center_local + corners[i] * bound_radius_m;
|
||||
clip[i] = viewproj * glm::vec4(p, 1.0f);
|
||||
}
|
||||
|
||||
auto all_out = [&](auto pred) {
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
if (!pred(clip[i])) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Clip volume in Vulkan (ZO): -w<=x<=w, -w<=y<=w, 0<=z<=w
|
||||
if (all_out([](const glm::vec4 &v) { return v.x < -v.w; })) return false; // left
|
||||
if (all_out([](const glm::vec4 &v) { return v.x > v.w; })) return false; // right
|
||||
if (all_out([](const glm::vec4 &v) { return v.y < -v.w; })) return false; // bottom
|
||||
if (all_out([](const glm::vec4 &v) { return v.y > v.w; })) return false; // top
|
||||
if (all_out([](const glm::vec4 &v) { return v.z < 0.0f; })) return false; // near (ZO)
|
||||
if (all_out([](const glm::vec4 &v) { return v.z > v.w; })) return false; // far
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void PlanetQuadtree::update(const WorldVec3 &body_center_world,
|
||||
double radius_m,
|
||||
const WorldVec3 &camera_world,
|
||||
const WorldVec3 &origin_world,
|
||||
const GPUSceneData &scene_data,
|
||||
VkExtent2D logical_extent)
|
||||
{
|
||||
_visible_leaves.clear();
|
||||
_stats = {};
|
||||
|
||||
if (radius_m <= 0.0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (logical_extent.width == 0 || logical_extent.height == 0)
|
||||
{
|
||||
logical_extent = VkExtent2D{1920, 1080};
|
||||
}
|
||||
|
||||
const bool rt_shadows_enabled = (scene_data.rtOptions.x != 0u) && (scene_data.rtOptions.z != 0u);
|
||||
const double cam_alt_m = glm::max(0.0, glm::length(camera_world - body_center_world) - radius_m);
|
||||
const bool rt_guardrail_active =
|
||||
_settings.rt_guardrail &&
|
||||
rt_shadows_enabled &&
|
||||
(_settings.max_patch_edge_rt_m > 0.0) &&
|
||||
(cam_alt_m <= _settings.rt_guardrail_max_altitude_m);
|
||||
|
||||
const float proj_y = scene_data.proj[1][1];
|
||||
const float proj_scale = std::abs(proj_y) * (static_cast<float>(logical_extent.height) * 0.5f);
|
||||
if (!(proj_scale > 0.0f))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
thread_local std::vector<Node> stack;
|
||||
stack.clear();
|
||||
stack.reserve(256);
|
||||
|
||||
const size_t max_visible_leaves =
|
||||
(_settings.max_patches_visible > 0u)
|
||||
? static_cast<size_t>(std::max(_settings.max_patches_visible, 6u))
|
||||
: std::numeric_limits<size_t>::max();
|
||||
|
||||
auto push_root = [&](CubeFace face)
|
||||
{
|
||||
Node n{};
|
||||
n.key.face = face;
|
||||
n.key.level = 0;
|
||||
n.key.x = 0;
|
||||
n.key.y = 0;
|
||||
stack.push_back(n);
|
||||
};
|
||||
|
||||
// Push in reverse order so pop_back visits in +X,-X,+Y,-Y,+Z,-Z order.
|
||||
push_root(CubeFace::NegZ);
|
||||
push_root(CubeFace::PosZ);
|
||||
push_root(CubeFace::NegY);
|
||||
push_root(CubeFace::PosY);
|
||||
push_root(CubeFace::NegX);
|
||||
push_root(CubeFace::PosX);
|
||||
|
||||
while (!stack.empty())
|
||||
{
|
||||
Node n = stack.back();
|
||||
stack.pop_back();
|
||||
_stats.nodes_visited++;
|
||||
|
||||
const PatchKey &k = n.key;
|
||||
|
||||
const double patch_edge_m = cubesphere_patch_edge_m(radius_m, k.level);
|
||||
const glm::dvec3 patch_dir = cubesphere_patch_center_direction(k.face, k.level, k.x, k.y);
|
||||
|
||||
double cos_patch_radius = 1.0;
|
||||
double sin_patch_radius = 0.0;
|
||||
double patch_bound_r_m = 1.0;
|
||||
if (_settings.horizon_cull || _settings.frustum_cull)
|
||||
{
|
||||
compute_patch_visibility_terms(k, patch_dir, radius_m, cos_patch_radius, sin_patch_radius, patch_bound_r_m);
|
||||
}
|
||||
|
||||
if (_settings.horizon_cull)
|
||||
{
|
||||
if (!is_patch_visible_horizon(body_center_world,
|
||||
radius_m,
|
||||
camera_world,
|
||||
patch_dir,
|
||||
cos_patch_radius,
|
||||
sin_patch_radius))
|
||||
{
|
||||
_stats.nodes_culled++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const WorldVec3 patch_center_world =
|
||||
body_center_world + patch_dir * radius_m;
|
||||
|
||||
if (_settings.frustum_cull)
|
||||
{
|
||||
const glm::vec3 patch_center_local = world_to_local(patch_center_world, origin_world);
|
||||
const float bound_r = static_cast<float>(patch_bound_r_m);
|
||||
if (!is_patch_visible_frustum(patch_center_local, bound_r, scene_data.viewproj))
|
||||
{
|
||||
_stats.nodes_culled++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const double dist_m = glm::max(1.0, glm::length(camera_world - patch_center_world));
|
||||
|
||||
// Screen-space error metric.
|
||||
const double error_m = 0.5 * patch_edge_m;
|
||||
const float sse_px = static_cast<float>((error_m / dist_m) * static_cast<double>(proj_scale));
|
||||
|
||||
bool refine = (k.level < _settings.max_level) && (sse_px > _settings.target_sse_px);
|
||||
if (!refine && rt_guardrail_active && (k.level < _settings.max_level) && (patch_edge_m > _settings.max_patch_edge_rt_m))
|
||||
{
|
||||
refine = true;
|
||||
}
|
||||
|
||||
if (refine)
|
||||
{
|
||||
// Budget check: splitting replaces this node with 4 children (adds +3 leaves minimum).
|
||||
// Keep a stable upper bound on the final leaf count: leaves_so_far + stack.size() + 4.
|
||||
const size_t min_leaves_if_split = _visible_leaves.size() + stack.size() + 4u;
|
||||
if (min_leaves_if_split > max_visible_leaves)
|
||||
{
|
||||
refine = false;
|
||||
_stats.splits_budget_limited++;
|
||||
}
|
||||
}
|
||||
|
||||
if (refine)
|
||||
{
|
||||
// Child order: (0,0), (1,0), (0,1), (1,1) with y increasing downward.
|
||||
const uint32_t cl = k.level + 1u;
|
||||
const uint32_t cx = k.x * 2u;
|
||||
const uint32_t cy = k.y * 2u;
|
||||
|
||||
stack.push_back(Node{PatchKey{k.face, cl, cx + 1u, cy + 1u}});
|
||||
stack.push_back(Node{PatchKey{k.face, cl, cx + 0u, cy + 1u}});
|
||||
stack.push_back(Node{PatchKey{k.face, cl, cx + 1u, cy + 0u}});
|
||||
stack.push_back(Node{PatchKey{k.face, cl, cx + 0u, cy + 0u}});
|
||||
continue;
|
||||
}
|
||||
|
||||
_visible_leaves.push_back(k);
|
||||
_stats.max_level_used = std::max(_stats.max_level_used, k.level);
|
||||
}
|
||||
|
||||
_stats.visible_leaves = static_cast<uint32_t>(_visible_leaves.size());
|
||||
|
||||
// Keep deterministic order for stability (optional).
|
||||
// DFS already stable; sort is useful when culling changes traversal.
|
||||
std::sort(_visible_leaves.begin(), _visible_leaves.end(),
|
||||
[](const PatchKey &a, const PatchKey &b)
|
||||
{
|
||||
if (a.face != b.face) return a.face < b.face;
|
||||
if (a.level != b.level) return a.level < b.level;
|
||||
if (a.x != b.x) return a.x < b.x;
|
||||
return a.y < b.y;
|
||||
});
|
||||
}
|
||||
} // namespace planet
|
||||
82
src/scene/planet/planet_quadtree.h
Normal file
82
src/scene/planet/planet_quadtree.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
|
||||
#include "cubesphere.h"
|
||||
|
||||
#include <core/types.h>
|
||||
#include <core/world.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace planet
|
||||
{
|
||||
struct PatchKey
|
||||
{
|
||||
CubeFace face = CubeFace::PosX;
|
||||
uint32_t level = 0;
|
||||
uint32_t x = 0;
|
||||
uint32_t y = 0;
|
||||
|
||||
friend bool operator==(const PatchKey &, const PatchKey &) = default;
|
||||
};
|
||||
|
||||
struct PatchKeyHash
|
||||
{
|
||||
size_t operator()(const PatchKey &k) const noexcept
|
||||
{
|
||||
const uint64_t f = static_cast<uint64_t>(k.face) & 0xFFull;
|
||||
const uint64_t l = static_cast<uint64_t>(k.level) & 0x3Full;
|
||||
const uint64_t x = static_cast<uint64_t>(k.x) & 0x1FFFFFull;
|
||||
const uint64_t y = static_cast<uint64_t>(k.y) & 0x1FFFFFull;
|
||||
|
||||
// Simple stable packing: [face:8 | level:6 | x:21 | y:21]
|
||||
const uint64_t packed = (f << 56) | (l << 50) | (x << 29) | (y << 8);
|
||||
return std::hash<uint64_t>{}(packed);
|
||||
}
|
||||
};
|
||||
|
||||
class PlanetQuadtree
|
||||
{
|
||||
public:
|
||||
struct Settings
|
||||
{
|
||||
uint32_t max_level = 14;
|
||||
float target_sse_px = 32.0f; // screen space error pixel
|
||||
uint32_t max_patches_visible = 8192;
|
||||
bool frustum_cull = true;
|
||||
bool horizon_cull = true;
|
||||
|
||||
// RT stability guardrail (only applied near-surface).
|
||||
bool rt_guardrail = true;
|
||||
double max_patch_edge_rt_m = 5000.0;
|
||||
double rt_guardrail_max_altitude_m = 200000.0;
|
||||
};
|
||||
|
||||
struct Stats
|
||||
{
|
||||
uint32_t visible_leaves = 0;
|
||||
uint32_t max_level_used = 0;
|
||||
uint32_t nodes_visited = 0;
|
||||
uint32_t nodes_culled = 0;
|
||||
uint32_t splits_budget_limited = 0;
|
||||
};
|
||||
|
||||
void set_settings(const Settings &settings) { _settings = settings; }
|
||||
const Settings &settings() const { return _settings; }
|
||||
const Stats &stats() const { return _stats; }
|
||||
const std::vector<PatchKey> &visible_leaves() const { return _visible_leaves; }
|
||||
|
||||
void update(const WorldVec3 &body_center_world,
|
||||
double radius_m,
|
||||
const WorldVec3 &camera_world,
|
||||
const WorldVec3 &origin_world,
|
||||
const GPUSceneData &scene_data,
|
||||
VkExtent2D logical_extent);
|
||||
|
||||
private:
|
||||
Settings _settings{};
|
||||
Stats _stats{};
|
||||
std::vector<PatchKey> _visible_leaves;
|
||||
};
|
||||
} // namespace planet
|
||||
989
src/scene/planet/planet_system.cpp
Normal file
989
src/scene/planet/planet_system.cpp
Normal file
@@ -0,0 +1,989 @@
|
||||
#include "planet_system.h"
|
||||
|
||||
#include <core/context.h>
|
||||
#include <core/device/resource.h>
|
||||
#include <core/frame/resources.h>
|
||||
#include <core/types.h>
|
||||
#include <core/assets/manager.h>
|
||||
#include <render/materials.h>
|
||||
#include <render/primitives.h>
|
||||
#include <core/pipeline/sampler.h>
|
||||
#include <scene/planet/cubesphere.h>
|
||||
#include <scene/tangent_space.h>
|
||||
#include <scene/vk_scene.h>
|
||||
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
|
||||
#include "device.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr double kEarthRadiusM = 6378137.0; // WGS84 equatorial radius
|
||||
constexpr double kMoonRadiusM = 1737400.0; // mean radius
|
||||
constexpr double kMoonDistanceM = 384400000.0; // mean Earth-Moon distance
|
||||
|
||||
struct PatchBoundsData
|
||||
{
|
||||
glm::vec3 origin{0.0f};
|
||||
glm::vec3 extents{0.5f};
|
||||
float sphere_radius{0.5f};
|
||||
};
|
||||
|
||||
PatchBoundsData compute_patch_bounds(const std::vector<Vertex> &vertices)
|
||||
{
|
||||
PatchBoundsData b{};
|
||||
if (vertices.empty())
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
glm::vec3 minpos = vertices[0].position;
|
||||
glm::vec3 maxpos = vertices[0].position;
|
||||
for (const auto &v : vertices)
|
||||
{
|
||||
minpos = glm::min(minpos, v.position);
|
||||
maxpos = glm::max(maxpos, v.position);
|
||||
}
|
||||
b.origin = (maxpos + minpos) * 0.5f;
|
||||
b.extents = (maxpos - minpos) * 0.5f;
|
||||
b.sphere_radius = glm::length(b.extents);
|
||||
return b;
|
||||
}
|
||||
|
||||
GLTFMetallic_Roughness::MaterialConstants make_planet_constants()
|
||||
{
|
||||
GLTFMetallic_Roughness::MaterialConstants c{};
|
||||
c.colorFactors = glm::vec4(1.0f);
|
||||
// metal_rough_factors.x = metallic, .y = roughness
|
||||
c.metal_rough_factors = glm::vec4(0.0f, 1.0f, 0.0f, 0.0f);
|
||||
return c;
|
||||
}
|
||||
|
||||
glm::vec4 debug_color_for_level(uint32_t level)
|
||||
{
|
||||
const float t = static_cast<float>(level) * 0.37f;
|
||||
const float r = 0.35f + 0.65f * std::sin(t + 0.0f);
|
||||
const float g = 0.35f + 0.65f * std::sin(t + 2.1f);
|
||||
const float b = 0.35f + 0.65f * std::sin(t + 4.2f);
|
||||
return glm::vec4(r, g, b, 1.0f);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void PlanetSystem::init(EngineContext *context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
void PlanetSystem::set_earth_debug_tint_patches_by_lod(bool enabled)
|
||||
{
|
||||
if (_earth_debug_tint_patches_by_lod == enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_earth_debug_tint_patches_by_lod = enabled;
|
||||
_earth_patch_cache_dirty = true;
|
||||
}
|
||||
|
||||
void PlanetSystem::cleanup()
|
||||
{
|
||||
if (!_context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TextureCache *textures = _context->textures;
|
||||
if (textures)
|
||||
{
|
||||
for (MaterialInstance &mat : _earth_face_materials)
|
||||
{
|
||||
if (mat.materialSet != VK_NULL_HANDLE)
|
||||
{
|
||||
textures->unwatchSet(mat.materialSet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResourceManager *rm = _context->getResources();
|
||||
if (rm)
|
||||
{
|
||||
for (EarthPatch &p : _earth_patches)
|
||||
{
|
||||
if (p.vertex_buffer.buffer != VK_NULL_HANDLE)
|
||||
{
|
||||
rm->destroy_buffer(p.vertex_buffer);
|
||||
p.vertex_buffer = {};
|
||||
p.vertex_buffer_address = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_earth_patch_index_buffer.buffer != VK_NULL_HANDLE)
|
||||
{
|
||||
rm->destroy_buffer(_earth_patch_index_buffer);
|
||||
_earth_patch_index_buffer = {};
|
||||
}
|
||||
|
||||
if (_earth_patch_material_constants_buffer.buffer != VK_NULL_HANDLE)
|
||||
{
|
||||
rm->destroy_buffer(_earth_patch_material_constants_buffer);
|
||||
_earth_patch_material_constants_buffer = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (_earth_patch_material_allocator_initialized)
|
||||
{
|
||||
if (DeviceManager *device = _context->getDevice())
|
||||
{
|
||||
_earth_patch_material_allocator.destroy_pools(device->device());
|
||||
}
|
||||
_earth_patch_material_allocator_initialized = false;
|
||||
}
|
||||
|
||||
if (_earth_patch_material_layout != VK_NULL_HANDLE)
|
||||
{
|
||||
if (DeviceManager *device = _context->getDevice())
|
||||
{
|
||||
vkDestroyDescriptorSetLayout(device->device(), _earth_patch_material_layout, nullptr);
|
||||
}
|
||||
_earth_patch_material_layout = VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
_earth_patch_lookup.clear();
|
||||
_earth_patch_lru.clear();
|
||||
_earth_patch_free.clear();
|
||||
_earth_patches.clear();
|
||||
_earth_face_materials = {};
|
||||
|
||||
_earth_patch_index_count = 0;
|
||||
_earth_patch_index_resolution = 0;
|
||||
_earth_patch_frame_stamp = 0;
|
||||
}
|
||||
|
||||
const PlanetSystem::PlanetBody *PlanetSystem::get_body(BodyID id) const
|
||||
{
|
||||
size_t i = static_cast<size_t>(id);
|
||||
if (i >= _bodies.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &_bodies[i];
|
||||
}
|
||||
|
||||
PlanetSystem::PlanetBody *PlanetSystem::get_body(BodyID id)
|
||||
{
|
||||
size_t i = static_cast<size_t>(id);
|
||||
if (i >= _bodies.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &_bodies[i];
|
||||
}
|
||||
|
||||
void PlanetSystem::ensure_bodies_created()
|
||||
{
|
||||
if (!_bodies.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlanetBody earth{};
|
||||
earth.name = "Earth";
|
||||
earth.center_world = WorldVec3(0.0, 0.0, 0.0);
|
||||
earth.radius_m = kEarthRadiusM;
|
||||
|
||||
PlanetBody moon{};
|
||||
moon.name = "Moon";
|
||||
moon.center_world = WorldVec3(kMoonDistanceM, 0.0, 0.0);
|
||||
moon.radius_m = kMoonRadiusM;
|
||||
|
||||
if (_context && _context->assets)
|
||||
{
|
||||
AssetManager *assets = _context->assets;
|
||||
|
||||
// Earth: cube-sphere quadtree patches (Milestones B2-B4). Material is shared.
|
||||
{
|
||||
GLTFMetallic_Roughness::MaterialConstants mc = make_planet_constants();
|
||||
mc.colorFactors = glm::vec4(1.0f);
|
||||
earth.material = assets->createMaterialFromConstants("Planet_EarthMaterial", mc, MaterialPass::MainColor);
|
||||
}
|
||||
|
||||
// Moon: constant albedo (no texture yet).
|
||||
{
|
||||
GLTFMetallic_Roughness::MaterialConstants mc = make_planet_constants();
|
||||
mc.colorFactors = glm::vec4(0.72f, 0.72f, 0.75f, 1.0f);
|
||||
|
||||
moon.material = assets->createMaterialFromConstants("Planet_MoonMaterial", mc, MaterialPass::MainColor);
|
||||
|
||||
std::vector<Vertex> verts;
|
||||
std::vector<uint32_t> inds;
|
||||
primitives::buildSphere(verts, inds, 48, 24);
|
||||
geom::generate_tangents(verts, inds);
|
||||
|
||||
moon.mesh = assets->createMesh("Planet_MoonSphere", verts, inds, moon.material);
|
||||
}
|
||||
}
|
||||
|
||||
_bodies.push_back(std::move(earth));
|
||||
_bodies.push_back(std::move(moon));
|
||||
}
|
||||
|
||||
PlanetSystem::EarthPatch *PlanetSystem::find_earth_patch(const planet::PatchKey &key)
|
||||
{
|
||||
auto it = _earth_patch_lookup.find(key);
|
||||
if (it == _earth_patch_lookup.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t idx = it->second;
|
||||
if (idx >= _earth_patches.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &_earth_patches[idx];
|
||||
}
|
||||
|
||||
void PlanetSystem::clear_earth_patch_cache()
|
||||
{
|
||||
if (!_context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceManager *rm = _context->getResources();
|
||||
FrameResources *frame = _context->currentFrame;
|
||||
|
||||
if (rm)
|
||||
{
|
||||
for (EarthPatch &p : _earth_patches)
|
||||
{
|
||||
if (p.vertex_buffer.buffer == VK_NULL_HANDLE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const AllocatedBuffer vb = p.vertex_buffer;
|
||||
if (frame)
|
||||
{
|
||||
frame->_deletionQueue.push_function([rm, vb]() { rm->destroy_buffer(vb); });
|
||||
}
|
||||
else
|
||||
{
|
||||
rm->destroy_buffer(vb);
|
||||
}
|
||||
|
||||
p.vertex_buffer = {};
|
||||
p.vertex_buffer_address = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_earth_patch_lookup.clear();
|
||||
_earth_patch_lru.clear();
|
||||
_earth_patch_free.clear();
|
||||
_earth_patches.clear();
|
||||
}
|
||||
|
||||
void PlanetSystem::ensure_earth_patch_index_buffer()
|
||||
{
|
||||
if (_earth_patch_index_buffer.buffer != VK_NULL_HANDLE && _earth_patch_index_resolution == _earth_patch_resolution)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceManager *rm = _context->getResources();
|
||||
if (!rm)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolution changed (or first init): clear existing patch cache and shared index buffer.
|
||||
if (_earth_patch_index_buffer.buffer != VK_NULL_HANDLE)
|
||||
{
|
||||
FrameResources *frame = _context->currentFrame;
|
||||
|
||||
// Destroy per-patch vertex buffers.
|
||||
for (const auto &kv : _earth_patch_lookup)
|
||||
{
|
||||
const uint32_t idx = kv.second;
|
||||
if (idx >= _earth_patches.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
EarthPatch &p = _earth_patches[idx];
|
||||
if (p.vertex_buffer.buffer == VK_NULL_HANDLE)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const AllocatedBuffer vb = p.vertex_buffer;
|
||||
if (frame)
|
||||
{
|
||||
frame->_deletionQueue.push_function([rm, vb]() { rm->destroy_buffer(vb); });
|
||||
}
|
||||
else
|
||||
{
|
||||
rm->destroy_buffer(vb);
|
||||
}
|
||||
}
|
||||
|
||||
_earth_patch_lookup.clear();
|
||||
_earth_patch_lru.clear();
|
||||
_earth_patch_free.clear();
|
||||
_earth_patches.clear();
|
||||
|
||||
const AllocatedBuffer ib = _earth_patch_index_buffer;
|
||||
if (frame)
|
||||
{
|
||||
frame->_deletionQueue.push_function([rm, ib]() { rm->destroy_buffer(ib); });
|
||||
}
|
||||
else
|
||||
{
|
||||
rm->destroy_buffer(ib);
|
||||
}
|
||||
_earth_patch_index_buffer = {};
|
||||
_earth_patch_index_count = 0;
|
||||
_earth_patch_index_resolution = 0;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> indices;
|
||||
planet::build_cubesphere_patch_indices(indices, _earth_patch_resolution);
|
||||
_earth_patch_index_count = static_cast<uint32_t>(indices.size());
|
||||
_earth_patch_index_buffer =
|
||||
rm->upload_buffer(indices.data(),
|
||||
indices.size() * sizeof(uint32_t),
|
||||
VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
|
||||
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR);
|
||||
_earth_patch_index_resolution = _earth_patch_resolution;
|
||||
}
|
||||
|
||||
void PlanetSystem::ensure_earth_patch_material_layout()
|
||||
{
|
||||
if (_earth_patch_material_layout != VK_NULL_HANDLE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceManager *device = _context->getDevice();
|
||||
if (!device)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DescriptorLayoutBuilder layoutBuilder;
|
||||
layoutBuilder.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
|
||||
layoutBuilder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
layoutBuilder.add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
layoutBuilder.add_binding(3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
layoutBuilder.add_binding(4, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
layoutBuilder.add_binding(5, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
|
||||
_earth_patch_material_layout = layoutBuilder.build(device->device(),
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
nullptr,
|
||||
VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
|
||||
}
|
||||
|
||||
void PlanetSystem::ensure_earth_patch_material_constants_buffer()
|
||||
{
|
||||
if (_earth_patch_material_constants_buffer.buffer != VK_NULL_HANDLE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceManager *rm = _context->getResources();
|
||||
DeviceManager *device = _context->getDevice();
|
||||
if (!rm || !device)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const GLTFMetallic_Roughness::MaterialConstants constants = make_planet_constants();
|
||||
|
||||
_earth_patch_material_constants_buffer =
|
||||
rm->create_buffer(sizeof(GLTFMetallic_Roughness::MaterialConstants),
|
||||
VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
|
||||
VMA_MEMORY_USAGE_CPU_TO_GPU);
|
||||
|
||||
if (_earth_patch_material_constants_buffer.buffer == VK_NULL_HANDLE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VmaAllocationInfo allocInfo{};
|
||||
vmaGetAllocationInfo(device->allocator(), _earth_patch_material_constants_buffer.allocation, &allocInfo);
|
||||
auto *mapped = static_cast<GLTFMetallic_Roughness::MaterialConstants *>(allocInfo.pMappedData);
|
||||
if (mapped)
|
||||
{
|
||||
*mapped = constants;
|
||||
vmaFlushAllocation(device->allocator(), _earth_patch_material_constants_buffer.allocation, 0, sizeof(constants));
|
||||
}
|
||||
}
|
||||
|
||||
void PlanetSystem::ensure_earth_face_materials(const PlanetBody &earth)
|
||||
{
|
||||
if (!_context || !earth.material)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DeviceManager *device = _context->getDevice();
|
||||
SamplerManager *samplers = _context->getSamplers();
|
||||
AssetManager *assets = _context->assets;
|
||||
TextureCache *textures = _context->textures;
|
||||
if (!device || !assets)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_earth_patch_material_layout();
|
||||
ensure_earth_patch_material_constants_buffer();
|
||||
|
||||
if (_earth_patch_material_layout == VK_NULL_HANDLE ||
|
||||
_earth_patch_material_constants_buffer.buffer == VK_NULL_HANDLE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_earth_patch_material_allocator_initialized)
|
||||
{
|
||||
std::vector<DescriptorAllocatorGrowable::PoolSizeRatio> sizes = {
|
||||
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1},
|
||||
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 6},
|
||||
};
|
||||
_earth_patch_material_allocator.init(device->device(), 16, sizes);
|
||||
_earth_patch_material_allocator_initialized = true;
|
||||
}
|
||||
|
||||
VkSampler tileSampler = samplers ? samplers->linearClampEdge() : VK_NULL_HANDLE;
|
||||
if (tileSampler == VK_NULL_HANDLE && samplers)
|
||||
{
|
||||
tileSampler = samplers->defaultLinear();
|
||||
}
|
||||
if (tileSampler == VK_NULL_HANDLE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VkImageView checker = assets->fallback_checkerboard_view();
|
||||
VkImageView white = assets->fallback_white_view();
|
||||
VkImageView flatNormal = assets->fallback_flat_normal_view();
|
||||
VkImageView black = assets->fallback_black_view();
|
||||
|
||||
if (checker == VK_NULL_HANDLE) checker = white;
|
||||
if (white == VK_NULL_HANDLE) white = checker;
|
||||
if (flatNormal == VK_NULL_HANDLE) flatNormal = white;
|
||||
if (black == VK_NULL_HANDLE) black = white;
|
||||
|
||||
auto face_legacy = [](planet::CubeFace f) -> const char * {
|
||||
switch (f)
|
||||
{
|
||||
case planet::CubeFace::PosX: return "px";
|
||||
case planet::CubeFace::NegX: return "nx";
|
||||
case planet::CubeFace::PosY: return "py";
|
||||
case planet::CubeFace::NegY: return "ny";
|
||||
case planet::CubeFace::PosZ: return "pz";
|
||||
case planet::CubeFace::NegZ: return "nz";
|
||||
}
|
||||
return "px";
|
||||
};
|
||||
|
||||
for (size_t face_index = 0; face_index < _earth_face_materials.size(); ++face_index)
|
||||
{
|
||||
MaterialInstance &mat = _earth_face_materials[face_index];
|
||||
|
||||
mat.pipeline = earth.material->data.pipeline;
|
||||
mat.passType = earth.material->data.passType;
|
||||
|
||||
if (mat.materialSet == VK_NULL_HANDLE)
|
||||
{
|
||||
mat.materialSet = _earth_patch_material_allocator.allocate(device->device(), _earth_patch_material_layout);
|
||||
|
||||
DescriptorWriter writer;
|
||||
writer.write_buffer(0,
|
||||
_earth_patch_material_constants_buffer.buffer,
|
||||
sizeof(GLTFMetallic_Roughness::MaterialConstants),
|
||||
0,
|
||||
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
|
||||
writer.write_image(1,
|
||||
checker,
|
||||
tileSampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
writer.write_image(2,
|
||||
white,
|
||||
tileSampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
writer.write_image(3,
|
||||
flatNormal,
|
||||
tileSampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
writer.write_image(4,
|
||||
white,
|
||||
tileSampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
writer.write_image(5,
|
||||
black,
|
||||
tileSampler,
|
||||
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
|
||||
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
writer.update_set(device->device(), mat.materialSet);
|
||||
|
||||
if (textures && tileSampler != VK_NULL_HANDLE)
|
||||
{
|
||||
const planet::CubeFace face = static_cast<planet::CubeFace>(face_index);
|
||||
const std::string rel = fmt::format("planets/earth/albedo/L0/{}.ktx2", face_legacy(face));
|
||||
|
||||
TextureCache::TextureKey tk{};
|
||||
tk.kind = TextureCache::TextureKey::SourceKind::FilePath;
|
||||
tk.path = assets->assetPath(rel);
|
||||
tk.srgb = true;
|
||||
tk.mipmapped = true;
|
||||
|
||||
TextureCache::TextureHandle h = textures->request(tk, tileSampler);
|
||||
textures->watchBinding(h, mat.materialSet, 1u, tileSampler, checker);
|
||||
textures->pin(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlanetSystem::EarthPatch *PlanetSystem::get_or_create_earth_patch(const PlanetBody &earth,
|
||||
const planet::PatchKey &key,
|
||||
uint32_t frame_index)
|
||||
{
|
||||
if (EarthPatch *p = find_earth_patch(key))
|
||||
{
|
||||
p->last_used_frame = frame_index;
|
||||
_earth_patch_lru.splice(_earth_patch_lru.begin(), _earth_patch_lru, p->lru_it);
|
||||
return p;
|
||||
}
|
||||
|
||||
if (!_context)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ResourceManager *rm = _context->getResources();
|
||||
DeviceManager *device = _context->getDevice();
|
||||
if (!rm || !device)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (_earth_patch_index_buffer.buffer == VK_NULL_HANDLE || _earth_patch_index_count == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const glm::vec4 vertex_color =
|
||||
_earth_debug_tint_patches_by_lod ? debug_color_for_level(key.level) : glm::vec4(1.0f);
|
||||
|
||||
std::vector<Vertex> vertices;
|
||||
const glm::dvec3 patch_center_dir =
|
||||
planet::build_cubesphere_patch_vertices(vertices,
|
||||
earth.radius_m,
|
||||
key.face,
|
||||
key.level,
|
||||
key.x,
|
||||
key.y,
|
||||
_earth_patch_resolution,
|
||||
vertex_color);
|
||||
|
||||
if (vertices.empty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const PatchBoundsData bounds = compute_patch_bounds(vertices);
|
||||
|
||||
AllocatedBuffer vb =
|
||||
rm->upload_buffer(vertices.data(),
|
||||
vertices.size() * sizeof(Vertex),
|
||||
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT |
|
||||
VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR);
|
||||
if (vb.buffer == VK_NULL_HANDLE)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VkBufferDeviceAddressInfo addrInfo{.sType = VK_STRUCTURE_TYPE_BUFFER_DEVICE_ADDRESS_INFO};
|
||||
addrInfo.buffer = vb.buffer;
|
||||
VkDeviceAddress addr = vkGetBufferDeviceAddress(device->device(), &addrInfo);
|
||||
|
||||
uint32_t idx = 0;
|
||||
if (!_earth_patch_free.empty())
|
||||
{
|
||||
idx = _earth_patch_free.back();
|
||||
_earth_patch_free.pop_back();
|
||||
}
|
||||
else
|
||||
{
|
||||
idx = static_cast<uint32_t>(_earth_patches.size());
|
||||
_earth_patches.emplace_back();
|
||||
}
|
||||
|
||||
if (idx >= _earth_patches.size())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EarthPatch &p = _earth_patches[idx];
|
||||
p.key = key;
|
||||
p.state = EarthPatchState::Ready;
|
||||
p.vertex_buffer = vb;
|
||||
p.vertex_buffer_address = addr;
|
||||
p.bounds_origin = bounds.origin;
|
||||
p.bounds_extents = bounds.extents;
|
||||
p.bounds_sphere_radius = bounds.sphere_radius;
|
||||
p.patch_center_dir = patch_center_dir;
|
||||
p.last_used_frame = frame_index;
|
||||
_earth_patch_lru.push_front(idx);
|
||||
p.lru_it = _earth_patch_lru.begin();
|
||||
|
||||
_earth_patch_lookup.emplace(key, idx);
|
||||
return &p;
|
||||
}
|
||||
|
||||
void PlanetSystem::trim_earth_patch_cache()
|
||||
{
|
||||
if (_earth_patch_cache_max == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_earth_patch_lookup.size() <= static_cast<size_t>(_earth_patch_cache_max))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ResourceManager *rm = _context->getResources();
|
||||
if (!rm)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FrameResources *frame = _context->currentFrame;
|
||||
const uint32_t now = _earth_patch_frame_stamp;
|
||||
|
||||
size_t guard = 0;
|
||||
const size_t guard_limit = _earth_patch_lru.size();
|
||||
|
||||
while (_earth_patch_lookup.size() > static_cast<size_t>(_earth_patch_cache_max) && !_earth_patch_lru.empty())
|
||||
{
|
||||
if (guard++ >= guard_limit)
|
||||
{
|
||||
// No evictable patches (all used this frame). Avoid thrashing.
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t idx = _earth_patch_lru.back();
|
||||
if (idx >= _earth_patches.size())
|
||||
{
|
||||
_earth_patch_lru.pop_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
EarthPatch &p = _earth_patches[idx];
|
||||
if (p.last_used_frame == now)
|
||||
{
|
||||
// Keep all patches referenced this frame.
|
||||
_earth_patch_lru.splice(_earth_patch_lru.begin(), _earth_patch_lru, p.lru_it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Made progress: we found an evictable patch.
|
||||
guard = 0;
|
||||
|
||||
_earth_patch_lru.erase(p.lru_it);
|
||||
_earth_patch_lookup.erase(p.key);
|
||||
|
||||
if (p.vertex_buffer.buffer != VK_NULL_HANDLE)
|
||||
{
|
||||
const AllocatedBuffer vb = p.vertex_buffer;
|
||||
if (frame)
|
||||
{
|
||||
frame->_deletionQueue.push_function([rm, vb]() { rm->destroy_buffer(vb); });
|
||||
}
|
||||
else
|
||||
{
|
||||
rm->destroy_buffer(vb);
|
||||
}
|
||||
}
|
||||
|
||||
p = EarthPatch{};
|
||||
_earth_patch_free.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
void PlanetSystem::update_and_emit(const SceneManager &scene, DrawContext &draw_context)
|
||||
{
|
||||
if (!_enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_bodies_created();
|
||||
|
||||
const WorldVec3 origin_world = scene.get_world_origin();
|
||||
|
||||
// Earth: quadtree patches.
|
||||
{
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
PlanetBody *earth = get_body(BodyID::Earth);
|
||||
if (earth && earth->visible && earth->material && _context)
|
||||
{
|
||||
if (_earth_patch_cache_dirty)
|
||||
{
|
||||
clear_earth_patch_cache();
|
||||
_earth_patch_cache_dirty = false;
|
||||
}
|
||||
|
||||
const Clock::time_point t0 = Clock::now();
|
||||
|
||||
_earth_quadtree.set_settings(_earth_quadtree_settings);
|
||||
|
||||
const VkExtent2D logical_extent = _context->getLogicalRenderExtent();
|
||||
const WorldVec3 cam_world = scene.getMainCamera().position_world;
|
||||
|
||||
const Clock::time_point t_q0 = Clock::now();
|
||||
_earth_quadtree.update(earth->center_world,
|
||||
earth->radius_m,
|
||||
cam_world,
|
||||
origin_world,
|
||||
scene.getSceneData(),
|
||||
logical_extent);
|
||||
const Clock::time_point t_q1 = Clock::now();
|
||||
|
||||
ensure_earth_patch_index_buffer();
|
||||
ensure_earth_face_materials(*earth);
|
||||
if (_context->textures)
|
||||
{
|
||||
for (const MaterialInstance &mat : _earth_face_materials)
|
||||
{
|
||||
if (mat.materialSet != VK_NULL_HANDLE)
|
||||
{
|
||||
_context->textures->markSetUsed(mat.materialSet, _context->frameIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t desired_capacity =
|
||||
static_cast<size_t>(_earth_patches.size()) +
|
||||
static_cast<size_t>(_earth_patch_create_budget_per_frame) +
|
||||
32u;
|
||||
if (_earth_patch_cache_max != 0)
|
||||
{
|
||||
desired_capacity = std::max(
|
||||
desired_capacity,
|
||||
static_cast<size_t>(_earth_patch_cache_max) +
|
||||
static_cast<size_t>(_earth_patch_create_budget_per_frame) +
|
||||
32u);
|
||||
}
|
||||
if (_earth_patches.capacity() < desired_capacity)
|
||||
{
|
||||
_earth_patches.reserve(desired_capacity);
|
||||
}
|
||||
|
||||
uint32_t created_patches = 0;
|
||||
double ms_patch_create = 0.0;
|
||||
const uint32_t max_create = _earth_patch_create_budget_per_frame;
|
||||
const double max_create_ms =
|
||||
(_earth_patch_create_budget_ms > 0.0f) ? static_cast<double>(_earth_patch_create_budget_ms) : 0.0;
|
||||
const uint32_t frame_index = ++_earth_patch_frame_stamp;
|
||||
|
||||
const Clock::time_point t_emit0 = Clock::now();
|
||||
std::vector<uint32_t> ready_patch_indices;
|
||||
ready_patch_indices.reserve(_earth_quadtree.visible_leaves().size());
|
||||
|
||||
for (const planet::PatchKey &k : _earth_quadtree.visible_leaves())
|
||||
{
|
||||
EarthPatch *patch = find_earth_patch(k);
|
||||
{
|
||||
if (patch)
|
||||
{
|
||||
patch->last_used_frame = frame_index;
|
||||
_earth_patch_lru.splice(_earth_patch_lru.begin(), _earth_patch_lru, patch->lru_it);
|
||||
}
|
||||
else
|
||||
{
|
||||
const bool hit_count_budget = (max_create != 0u) && (created_patches >= max_create);
|
||||
const bool hit_time_budget = (max_create_ms > 0.0) && (ms_patch_create >= max_create_ms);
|
||||
if (!hit_count_budget && !hit_time_budget)
|
||||
{
|
||||
const Clock::time_point t_c0 = Clock::now();
|
||||
patch = get_or_create_earth_patch(*earth, k, frame_index);
|
||||
const Clock::time_point t_c1 = Clock::now();
|
||||
|
||||
if (patch)
|
||||
{
|
||||
created_patches++;
|
||||
}
|
||||
ms_patch_create += std::chrono::duration<double, std::milli>(t_c1 - t_c0).count();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (patch)
|
||||
{
|
||||
const uint32_t idx = static_cast<uint32_t>(patch - _earth_patches.data());
|
||||
ready_patch_indices.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t idx : ready_patch_indices)
|
||||
{
|
||||
if (idx >= _earth_patches.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EarthPatch &patch = _earth_patches[idx];
|
||||
if (patch.state != EarthPatchState::Ready ||
|
||||
patch.vertex_buffer.buffer == VK_NULL_HANDLE ||
|
||||
patch.vertex_buffer_address == 0 ||
|
||||
_earth_patch_index_buffer.buffer == VK_NULL_HANDLE ||
|
||||
_earth_patch_index_count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t face_index = static_cast<uint32_t>(patch.key.face);
|
||||
if (face_index >= _earth_face_materials.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MaterialInstance *material = &_earth_face_materials[face_index];
|
||||
if (material->materialSet == VK_NULL_HANDLE || material->pipeline == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const WorldVec3 patch_center_world =
|
||||
earth->center_world + patch.patch_center_dir * earth->radius_m;
|
||||
const glm::vec3 patch_center_local = world_to_local(patch_center_world, origin_world);
|
||||
const glm::mat4 transform = glm::translate(glm::mat4(1.0f), patch_center_local);
|
||||
|
||||
Bounds b{};
|
||||
b.origin = patch.bounds_origin;
|
||||
b.extents = patch.bounds_extents;
|
||||
b.sphereRadius = patch.bounds_sphere_radius;
|
||||
b.type = BoundsType::Box;
|
||||
|
||||
RenderObject obj{};
|
||||
obj.indexCount = _earth_patch_index_count;
|
||||
obj.firstIndex = 0;
|
||||
obj.indexBuffer = _earth_patch_index_buffer.buffer;
|
||||
obj.vertexBuffer = patch.vertex_buffer.buffer;
|
||||
obj.vertexBufferAddress = patch.vertex_buffer_address;
|
||||
obj.material = material;
|
||||
obj.bounds = b;
|
||||
obj.transform = transform;
|
||||
// Planet terrain patches are not meaningful RT occluders; skip BLAS/TLAS builds.
|
||||
obj.sourceMesh = nullptr;
|
||||
obj.surfaceIndex = 0;
|
||||
obj.objectID = draw_context.nextID++;
|
||||
obj.ownerType = RenderObject::OwnerType::MeshInstance;
|
||||
obj.ownerName = earth->name;
|
||||
|
||||
draw_context.OpaqueSurfaces.push_back(obj);
|
||||
}
|
||||
const Clock::time_point t_emit1 = Clock::now();
|
||||
|
||||
trim_earth_patch_cache();
|
||||
|
||||
const uint32_t visible_patches = static_cast<uint32_t>(_earth_quadtree.visible_leaves().size());
|
||||
const uint32_t n = _earth_patch_resolution;
|
||||
const uint32_t patch_tris = (n >= 2u) ? (2u * (n - 1u) * (n + 3u)) : 0u;
|
||||
const uint32_t estimated_tris = patch_tris * visible_patches;
|
||||
|
||||
_earth_debug_stats = {};
|
||||
_earth_debug_stats.quadtree = _earth_quadtree.stats();
|
||||
_earth_debug_stats.visible_patches = visible_patches;
|
||||
_earth_debug_stats.created_patches = created_patches;
|
||||
_earth_debug_stats.patch_cache_size = static_cast<uint32_t>(_earth_patch_lookup.size());
|
||||
_earth_debug_stats.estimated_triangles = estimated_tris;
|
||||
_earth_debug_stats.ms_quadtree = static_cast<float>(std::chrono::duration<double, std::milli>(t_q1 - t_q0).count());
|
||||
_earth_debug_stats.ms_patch_create = static_cast<float>(ms_patch_create);
|
||||
const double ms_emit_total = std::chrono::duration<double, std::milli>(t_emit1 - t_emit0).count();
|
||||
_earth_debug_stats.ms_emit = static_cast<float>(std::max(0.0, ms_emit_total - ms_patch_create));
|
||||
_earth_debug_stats.ms_total = static_cast<float>(std::chrono::duration<double, std::milli>(Clock::now() - t0).count());
|
||||
}
|
||||
}
|
||||
|
||||
// Other bodies (moon etc.): regular mesh instances.
|
||||
for (size_t body_index = 0; body_index < _bodies.size(); ++body_index)
|
||||
{
|
||||
PlanetBody &b = _bodies[body_index];
|
||||
|
||||
if (body_index == static_cast<size_t>(BodyID::Earth))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!b.visible || !b.mesh || b.mesh->surfaces.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const glm::vec3 t_local = world_to_local(b.center_world, origin_world);
|
||||
const float r = static_cast<float>(b.radius_m);
|
||||
const glm::vec3 s = glm::vec3(r * 2.0f); // primitive sphere radius is 0.5
|
||||
const glm::quat q = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
const glm::mat4 transform = make_trs_matrix(t_local, q, s);
|
||||
|
||||
uint32_t surface_index = 0;
|
||||
for (const GeoSurface &surf : b.mesh->surfaces)
|
||||
{
|
||||
RenderObject obj{};
|
||||
obj.indexCount = surf.count;
|
||||
obj.firstIndex = surf.startIndex;
|
||||
obj.indexBuffer = b.mesh->meshBuffers.indexBuffer.buffer;
|
||||
obj.vertexBuffer = b.mesh->meshBuffers.vertexBuffer.buffer;
|
||||
obj.vertexBufferAddress = b.mesh->meshBuffers.vertexBufferAddress;
|
||||
obj.material = surf.material ? &surf.material->data : nullptr;
|
||||
obj.bounds = surf.bounds;
|
||||
obj.transform = transform;
|
||||
obj.sourceMesh = b.mesh.get();
|
||||
obj.surfaceIndex = surface_index++;
|
||||
obj.objectID = draw_context.nextID++;
|
||||
obj.ownerType = RenderObject::OwnerType::MeshInstance;
|
||||
obj.ownerName = b.name;
|
||||
|
||||
if (obj.material && obj.material->passType == MaterialPass::Transparent)
|
||||
{
|
||||
draw_context.TransparentSurfaces.push_back(obj);
|
||||
}
|
||||
else
|
||||
{
|
||||
draw_context.OpaqueSurfaces.push_back(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
148
src/scene/planet/planet_system.h
Normal file
148
src/scene/planet/planet_system.h
Normal file
@@ -0,0 +1,148 @@
|
||||
#pragma once
|
||||
|
||||
#include <core/world.h>
|
||||
#include <core/descriptor/descriptors.h>
|
||||
#include <scene/planet/planet_quadtree.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class EngineContext;
|
||||
class SceneManager;
|
||||
struct DrawContext;
|
||||
class MeshAsset;
|
||||
struct GLTFMaterial;
|
||||
|
||||
class PlanetSystem
|
||||
{
|
||||
public:
|
||||
enum class BodyID : uint8_t
|
||||
{
|
||||
Earth = 0,
|
||||
Moon = 1,
|
||||
};
|
||||
|
||||
struct EarthDebugStats
|
||||
{
|
||||
planet::PlanetQuadtree::Stats quadtree{};
|
||||
uint32_t visible_patches = 0;
|
||||
uint32_t created_patches = 0;
|
||||
uint32_t patch_cache_size = 0;
|
||||
uint32_t estimated_triangles = 0;
|
||||
float ms_quadtree = 0.0f;
|
||||
float ms_patch_create = 0.0f;
|
||||
float ms_emit = 0.0f;
|
||||
float ms_total = 0.0f;
|
||||
};
|
||||
|
||||
struct PlanetBody
|
||||
{
|
||||
std::string name;
|
||||
WorldVec3 center_world{0.0, 0.0, 0.0};
|
||||
double radius_m = 1.0;
|
||||
bool visible = true;
|
||||
|
||||
std::shared_ptr<MeshAsset> mesh;
|
||||
std::shared_ptr<GLTFMaterial> material;
|
||||
};
|
||||
|
||||
void init(EngineContext *context);
|
||||
void cleanup();
|
||||
|
||||
void update_and_emit(const SceneManager &scene, DrawContext &draw_context);
|
||||
|
||||
bool enabled() const { return _enabled; }
|
||||
void set_enabled(bool enabled) { _enabled = enabled; }
|
||||
|
||||
const PlanetBody *get_body(BodyID id) const;
|
||||
PlanetBody *get_body(BodyID id);
|
||||
const std::vector<PlanetBody> &bodies() const { return _bodies; }
|
||||
|
||||
const planet::PlanetQuadtree::Settings &earth_quadtree_settings() const { return _earth_quadtree_settings; }
|
||||
void set_earth_quadtree_settings(const planet::PlanetQuadtree::Settings &settings) { _earth_quadtree_settings = settings; }
|
||||
const EarthDebugStats &earth_debug_stats() const { return _earth_debug_stats; }
|
||||
|
||||
uint32_t earth_patch_create_budget_per_frame() const { return _earth_patch_create_budget_per_frame; }
|
||||
void set_earth_patch_create_budget_per_frame(uint32_t budget) { _earth_patch_create_budget_per_frame = budget; }
|
||||
|
||||
float earth_patch_create_budget_ms() const { return _earth_patch_create_budget_ms; }
|
||||
void set_earth_patch_create_budget_ms(float budget_ms) { _earth_patch_create_budget_ms = budget_ms; }
|
||||
|
||||
uint32_t earth_patch_cache_max() const { return _earth_patch_cache_max; }
|
||||
void set_earth_patch_cache_max(uint32_t max_patches) { _earth_patch_cache_max = max_patches; }
|
||||
|
||||
bool earth_debug_tint_patches_by_lod() const { return _earth_debug_tint_patches_by_lod; }
|
||||
void set_earth_debug_tint_patches_by_lod(bool enabled);
|
||||
|
||||
private:
|
||||
enum class EarthPatchState : uint8_t
|
||||
{
|
||||
Allocating = 0,
|
||||
Ready = 1,
|
||||
};
|
||||
|
||||
struct EarthPatch
|
||||
{
|
||||
planet::PatchKey key{};
|
||||
EarthPatchState state = EarthPatchState::Allocating;
|
||||
|
||||
AllocatedBuffer vertex_buffer{};
|
||||
VkDeviceAddress vertex_buffer_address = 0;
|
||||
|
||||
glm::vec3 bounds_origin{0.0f};
|
||||
glm::vec3 bounds_extents{0.5f};
|
||||
float bounds_sphere_radius = 0.5f;
|
||||
|
||||
WorldVec3 patch_center_dir{0.0, 0.0, 1.0};
|
||||
uint32_t last_used_frame = 0;
|
||||
std::list<uint32_t>::iterator lru_it{};
|
||||
};
|
||||
|
||||
void ensure_bodies_created();
|
||||
EarthPatch *find_earth_patch(const planet::PatchKey &key);
|
||||
EarthPatch *get_or_create_earth_patch(const PlanetBody &earth,
|
||||
const planet::PatchKey &key,
|
||||
uint32_t frame_index);
|
||||
void ensure_earth_patch_index_buffer();
|
||||
void ensure_earth_patch_material_layout();
|
||||
void ensure_earth_patch_material_constants_buffer();
|
||||
void ensure_earth_face_materials(const PlanetBody &earth);
|
||||
void clear_earth_patch_cache();
|
||||
void trim_earth_patch_cache();
|
||||
|
||||
EngineContext *_context = nullptr;
|
||||
bool _enabled = true;
|
||||
std::vector<PlanetBody> _bodies;
|
||||
|
||||
// Earth cube-sphere quadtree
|
||||
planet::PlanetQuadtree _earth_quadtree{};
|
||||
planet::PlanetQuadtree::Settings _earth_quadtree_settings{};
|
||||
EarthDebugStats _earth_debug_stats{};
|
||||
std::unordered_map<planet::PatchKey, uint32_t, planet::PatchKeyHash> _earth_patch_lookup;
|
||||
std::vector<EarthPatch> _earth_patches;
|
||||
std::vector<uint32_t> _earth_patch_free;
|
||||
std::list<uint32_t> _earth_patch_lru;
|
||||
AllocatedBuffer _earth_patch_index_buffer{};
|
||||
uint32_t _earth_patch_index_count = 0;
|
||||
uint32_t _earth_patch_index_resolution = 0;
|
||||
|
||||
VkDescriptorSetLayout _earth_patch_material_layout = VK_NULL_HANDLE;
|
||||
DescriptorAllocatorGrowable _earth_patch_material_allocator{};
|
||||
bool _earth_patch_material_allocator_initialized = false;
|
||||
AllocatedBuffer _earth_patch_material_constants_buffer{};
|
||||
std::array<MaterialInstance, 6> _earth_face_materials{};
|
||||
|
||||
uint32_t _earth_patch_frame_stamp = 0;
|
||||
uint32_t _earth_patch_resolution = 33;
|
||||
uint32_t _earth_patch_create_budget_per_frame = 16;
|
||||
float _earth_patch_create_budget_ms = 2.0f;
|
||||
uint32_t _earth_patch_cache_max = 2048;
|
||||
|
||||
bool _earth_debug_tint_patches_by_lod = false;
|
||||
bool _earth_patch_cache_dirty = false;
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <unordered_set>
|
||||
#include <chrono>
|
||||
|
||||
#include "scene/planet/planet_system.h"
|
||||
#include "core/device/swapchain.h"
|
||||
#include "core/context.h"
|
||||
#include "core/config.h"
|
||||
@@ -19,6 +20,8 @@
|
||||
#include "core/config.h"
|
||||
#include <fmt/core.h>
|
||||
|
||||
SceneManager::SceneManager() = default;
|
||||
|
||||
SceneManager::~SceneManager()
|
||||
{
|
||||
fmt::println("[SceneManager] dtor: loadedScenes={} dynamicGLTFInstances={} pendingGLTFRelease={}",
|
||||
@@ -111,7 +114,6 @@ void SceneManager::init(EngineContext *context)
|
||||
{
|
||||
_context = context;
|
||||
|
||||
mainCamera.velocity = glm::vec3(0.f);
|
||||
mainCamera.position_world = WorldVec3(30.0, 0.0, 85.0);
|
||||
mainCamera.orientation = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
@@ -119,7 +121,12 @@ void SceneManager::init(EngineContext *context)
|
||||
sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f);
|
||||
sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f);
|
||||
|
||||
cameraRig.init(*this, mainCamera);
|
||||
|
||||
_camera_position_local = world_to_local(mainCamera.position_world, _origin_world);
|
||||
|
||||
_planetSystem = std::make_unique<PlanetSystem>();
|
||||
_planetSystem->init(_context);
|
||||
}
|
||||
|
||||
void SceneManager::update_scene()
|
||||
@@ -151,7 +158,25 @@ void SceneManager::update_scene()
|
||||
mainDrawContext.nextID = 1;
|
||||
mainDrawContext.gltfNodeLocalOverrides = nullptr;
|
||||
|
||||
mainCamera.update();
|
||||
// Simple per-frame dt (seconds) for animations and camera behavior.
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (_lastFrameTime.time_since_epoch().count() == 0)
|
||||
{
|
||||
_lastFrameTime = now;
|
||||
}
|
||||
float dt = std::chrono::duration<float>(now - _lastFrameTime).count();
|
||||
_lastFrameTime = now;
|
||||
if (dt < 0.f)
|
||||
{
|
||||
dt = 0.f;
|
||||
}
|
||||
if (dt > 0.1f)
|
||||
{
|
||||
dt = 0.1f;
|
||||
}
|
||||
_deltaTime = dt;
|
||||
|
||||
cameraRig.update(*this, mainCamera, dt);
|
||||
|
||||
// Floating origin: keep render-local coordinates near (0,0,0) by shifting the origin
|
||||
// when the camera drifts too far in world space.
|
||||
@@ -171,24 +196,6 @@ void SceneManager::update_scene()
|
||||
|
||||
_camera_position_local = world_to_local(mainCamera.position_world, _origin_world);
|
||||
|
||||
// Simple per-frame dt (seconds) for animations
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (_lastFrameTime.time_since_epoch().count() == 0)
|
||||
{
|
||||
_lastFrameTime = now;
|
||||
}
|
||||
float dt = std::chrono::duration<float>(now - _lastFrameTime).count();
|
||||
_lastFrameTime = now;
|
||||
if (dt < 0.f)
|
||||
{
|
||||
dt = 0.f;
|
||||
}
|
||||
if (dt > 0.1f)
|
||||
{
|
||||
dt = 0.1f;
|
||||
}
|
||||
_deltaTime = dt;
|
||||
|
||||
auto tagOwner = [&](RenderObject::OwnerType type, const std::string &name,
|
||||
size_t opaqueBegin, size_t transpBegin)
|
||||
{
|
||||
@@ -425,6 +432,11 @@ void SceneManager::update_scene()
|
||||
sceneData.rtParams = glm::vec4(ss.hybridRayNoLThreshold, ss.enabled ? 1.0f : 0.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
if (_planetSystem)
|
||||
{
|
||||
_planetSystem->update_and_emit(*this, mainDrawContext);
|
||||
}
|
||||
|
||||
// Fill punctual lights into GPUSceneData
|
||||
const uint32_t lightCount = static_cast<uint32_t>(std::min(pointLights.size(), static_cast<size_t>(kMaxPunctualLights)));
|
||||
for (uint32_t i = 0; i < lightCount; ++i)
|
||||
@@ -515,6 +527,12 @@ std::shared_ptr<LoadedGLTF> SceneManager::getScene(const std::string &name)
|
||||
|
||||
void SceneManager::cleanup()
|
||||
{
|
||||
if (_planetSystem)
|
||||
{
|
||||
_planetSystem->cleanup();
|
||||
_planetSystem.reset();
|
||||
}
|
||||
|
||||
// Explicitly clear dynamic instances first to drop any extra shared_ptrs
|
||||
// that could keep GPU resources alive.
|
||||
clearMeshInstances();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <core/types.h>
|
||||
#include <core/world.h>
|
||||
#include <scene/camera.h>
|
||||
#include <scene/camera/camera_rig.h>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
@@ -11,6 +12,7 @@
|
||||
|
||||
#include "scene/vk_loader.h"
|
||||
class EngineContext;
|
||||
class PlanetSystem;
|
||||
|
||||
struct RenderObject
|
||||
{
|
||||
@@ -60,6 +62,7 @@ struct DrawContext
|
||||
class SceneManager
|
||||
{
|
||||
public:
|
||||
SceneManager();
|
||||
~SceneManager();
|
||||
void init(EngineContext *context);
|
||||
|
||||
@@ -68,6 +71,9 @@ public:
|
||||
void update_scene();
|
||||
|
||||
Camera &getMainCamera() { return mainCamera; }
|
||||
const Camera &getMainCamera() const { return mainCamera; }
|
||||
CameraRig &getCameraRig() { return cameraRig; }
|
||||
const CameraRig &getCameraRig() const { return cameraRig; }
|
||||
|
||||
WorldVec3 get_world_origin() const { return _origin_world; }
|
||||
glm::vec3 get_camera_local_position() const { return _camera_position_local; }
|
||||
@@ -220,6 +226,8 @@ public:
|
||||
|
||||
const PickingDebug &getPickingDebug() const { return pickingDebug; }
|
||||
|
||||
PlanetSystem *get_planet_system() const { return _planetSystem.get(); }
|
||||
|
||||
// Returns the LoadedGLTF scene for a named GLTF instance, or nullptr if not found.
|
||||
std::shared_ptr<LoadedGLTF> getGLTFInstanceScene(const std::string &instanceName) const;
|
||||
|
||||
@@ -227,6 +235,7 @@ private:
|
||||
EngineContext *_context = nullptr;
|
||||
|
||||
Camera mainCamera = {};
|
||||
CameraRig cameraRig{};
|
||||
GPUSceneData sceneData = {};
|
||||
DrawContext mainDrawContext;
|
||||
std::vector<PointLight> pointLights;
|
||||
@@ -248,5 +257,7 @@ private:
|
||||
// GPU resources that might still be in-flight.
|
||||
std::vector<std::shared_ptr<LoadedGLTF>> pendingGLTFRelease;
|
||||
|
||||
std::unique_ptr<PlanetSystem> _planetSystem;
|
||||
|
||||
PickingDebug pickingDebug{};
|
||||
};
|
||||
|
||||
7
third_party/CMakeLists.txt
vendored
7
third_party/CMakeLists.txt
vendored
@@ -1,5 +1,3 @@
|
||||
find_package(Vulkan REQUIRED)
|
||||
|
||||
add_library(vkbootstrap STATIC)
|
||||
add_library(glm INTERFACE)
|
||||
add_library(vma INTERFACE)
|
||||
@@ -62,11 +60,6 @@ target_include_directories(ImGuizmo PUBLIC
|
||||
imgui
|
||||
)
|
||||
|
||||
target_compile_definitions(ImGuizmo
|
||||
PRIVATE
|
||||
IMGUI_DEFINE_MATH_OPERATORS
|
||||
)
|
||||
|
||||
target_link_libraries(ImGuizmo PUBLIC imgui)
|
||||
|
||||
add_subdirectory(BVH)
|
||||
|
||||
Reference in New Issue
Block a user