Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ff457d498 | ||
|
|
a54d3cebf7 | ||
|
|
f1fa3ef780 | ||
|
|
2bf97defcd | ||
|
|
42645a31ea | ||
|
|
56e10d9983 | ||
|
|
dd97019264 | ||
|
|
ea2b2c457c | ||
|
|
c92a8e6e0b | ||
|
|
0ca3a5b8f1 |
@@ -0,0 +1 @@
|
||||
bin/** filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -1,5 +1,4 @@
|
||||
/build
|
||||
/bin
|
||||
/assets
|
||||
/.idea
|
||||
/cmake-build-debug
|
||||
|
||||
+54
-38
@@ -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()
|
||||
|
||||
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
||||
libSDL2-2.0d.so.0
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
libSDL2-2.0d.so.0.3200.10
|
||||
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
LFS
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
-1
@@ -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
|
||||
|
||||
|
||||
+160
-45
@@ -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.
|
||||
|
||||
+117
-8
@@ -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
|
||||
);
|
||||
|
||||
+86
-17
@@ -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;
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+584
-278
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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{};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
+38
-20
@@ -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{};
|
||||
};
|
||||
|
||||
Vendored
-7
@@ -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