Skip to content

Commit cee35d5

Browse files
perf(viewport): render camera frustums with GPU instancing (#1312)
* perf(viewport): render camera frustums with GPU instancing Camera frustum overlays were rebuilt and re-uploaded on the CPU every frame for every visible camera (~160k vertices/frame for large COLMAP datasets), causing viewport lag while navigating with frustums enabled. The cost was CPU/driver-bound, so it was most noticeable on Windows. Perspective frustums now draw via a single instanced draw per viewport panel: one storage-buffer instance per camera (model matrix + color), with the vertex shader generating the 8 edges, projecting them with the same camera-space pinhole as the scene, and expanding them into anti-aliased, depth-faded lines (reusing the shape-overlay fragment logic). Thumbnails and equirectangular frustums keep the existing path. Co-Authored-By: Oz <oz-agent@warp.dev> * fix(viewport): handle pano frustum instancing * refactor(viewport): trim frustum instancing path --------- Co-authored-by: Janusch Patas <janusch.patas@gmx.de>
1 parent 09deb39 commit cee35d5

5 files changed

Lines changed: 576 additions & 191 deletions

File tree

src/visualizer/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/textured_overla
291291
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/textured_overlay.frag" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/textured_overlay.frag.spv.h" kTexturedOverlayFragSpv)
292292
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/pivot.vert" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/pivot.vert.spv.h" kPivotVertSpv)
293293
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/pivot.frag" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/pivot.frag.spv.h" kPivotFragSpv)
294+
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/frustum.vert" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/frustum.vert.spv.h" kFrustumVertSpv)
294295
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/mesh.vert" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/mesh.vert.spv.h" kMeshVertSpv)
295296
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/mesh.frag" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/mesh.frag.spv.h" kMeshFragSpv)
296297
compile_shader(lfs_visualizer "${LFS_VIEWPORT_SHADER_SOURCE_DIR}/mesh_shadow.vert" "${LFS_VIEWPORT_SHADER_BINARY_DIR}/mesh_shadow.vert.spv.h" kMeshShadowVertSpv)

src/visualizer/gui/gui_manager.cpp

Lines changed: 75 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -973,57 +973,6 @@ namespace lfs::vis::gui {
973973
};
974974
}
975975

976-
[[nodiscard]] std::optional<glm::vec2> projectPointToPanelScreen(
977-
const VulkanGuidePanelTarget& panel,
978-
const RenderSettings& settings,
979-
const glm::vec3& world) {
980-
const glm::mat3 rotation = panel.viewport->getRotationMatrix();
981-
const glm::vec3 translation = panel.viewport->getTranslation();
982-
const glm::vec3 view = glm::transpose(rotation) * (world - translation);
983-
984-
if (settings.equirectangular) {
985-
const float len = glm::length(view);
986-
if (!std::isfinite(len) || len <= 1e-6f) {
987-
return std::nullopt;
988-
}
989-
const glm::vec3 dir = view / len;
990-
const float ndc_x = std::atan2(dir.x, -dir.z) / glm::pi<float>();
991-
const float ndc_y = -std::asin(std::clamp(dir.y, -1.0f, 1.0f)) /
992-
(glm::pi<float>() * 0.5f);
993-
if (!std::isfinite(ndc_x) || !std::isfinite(ndc_y)) {
994-
return std::nullopt;
995-
}
996-
return panel.pos + glm::vec2((ndc_x * 0.5f + 0.5f) * panel.size.x,
997-
(ndc_y * 0.5f + 0.5f) * panel.size.y);
998-
}
999-
1000-
constexpr float kMinViewZ = -1e-4f;
1001-
if (view.z >= kMinViewZ) {
1002-
return std::nullopt;
1003-
}
1004-
1005-
const float width = static_cast<float>(std::max(panel.render_size.x, 1));
1006-
const float height = static_cast<float>(std::max(panel.render_size.y, 1));
1007-
const float cx = width * 0.5f;
1008-
const float cy = height * 0.5f;
1009-
if (settings.orthographic) {
1010-
if (!std::isfinite(settings.ortho_scale) || settings.ortho_scale <= 0.0f) {
1011-
return std::nullopt;
1012-
}
1013-
return renderToPanelScreen(panel, glm::vec2(cx + view.x * settings.ortho_scale,
1014-
cy - view.y * settings.ortho_scale));
1015-
}
1016-
1017-
const auto [fx, fy] = lfs::rendering::computePixelFocalLengths(
1018-
panel.render_size, settings.focal_length_mm);
1019-
const float depth = -view.z;
1020-
if (depth <= 0.0f) {
1021-
return std::nullopt;
1022-
}
1023-
return renderToPanelScreen(panel, glm::vec2(cx + view.x * fx / depth,
1024-
cy - view.y * fy / depth));
1025-
}
1026-
1027976
[[nodiscard]] bool projectedQuadVisible(const std::array<glm::vec2, 4>& points,
1028977
const VulkanGuidePanelTarget& panel) {
1029978
glm::vec2 min_point(std::numeric_limits<float>::max());
@@ -2068,46 +2017,6 @@ namespace lfs::vis::gui {
20682017
return visualizer_camera_to_world * fov_scale;
20692018
}
20702019

2071-
void appendPerspectiveCameraFrustum(VulkanViewportPassParams& params,
2072-
const VulkanGuidePanelTarget& panel,
2073-
const RenderSettings& settings,
2074-
const glm::mat4& model,
2075-
const glm::vec4& color) {
2076-
constexpr std::array local_points{
2077-
glm::vec3(-0.5f, -0.5f, -1.0f),
2078-
glm::vec3(0.5f, -0.5f, -1.0f),
2079-
glm::vec3(0.5f, 0.5f, -1.0f),
2080-
glm::vec3(-0.5f, 0.5f, -1.0f),
2081-
glm::vec3(0.0f, 0.0f, 0.0f),
2082-
};
2083-
constexpr std::array<std::pair<int, int>, 8> edges{{
2084-
{0, 1},
2085-
{1, 2},
2086-
{2, 3},
2087-
{3, 0},
2088-
{0, 4},
2089-
{1, 4},
2090-
{2, 4},
2091-
{3, 4},
2092-
}};
2093-
2094-
std::array<glm::vec3, local_points.size()> world_points{};
2095-
for (size_t i = 0; i < local_points.size(); ++i) {
2096-
world_points[i] = glm::vec3(model * glm::vec4(local_points[i], 1.0f));
2097-
}
2098-
for (const auto& [a, b] : edges) {
2099-
addProjectedOverlayLine(params.overlay_triangles,
2100-
params,
2101-
panel,
2102-
settings,
2103-
world_points[static_cast<size_t>(a)],
2104-
world_points[static_cast<size_t>(b)],
2105-
color,
2106-
1.5f,
2107-
true);
2108-
}
2109-
}
2110-
21112020
void appendEquirectangularCameraFrustum(VulkanViewportPassParams& params,
21122021
const VulkanGuidePanelTarget& panel,
21132022
const RenderSettings& settings,
@@ -2218,9 +2127,58 @@ namespace lfs::vis::gui {
22182127
}
22192128

22202129
constexpr float kMinRenderAlpha = 0.01f;
2221-
const glm::vec3 view_position = panel.viewport->getTranslation();
2130+
const glm::mat3 panel_rotation = panel.viewport->getRotationMatrix();
2131+
const glm::vec3 panel_translation = panel.viewport->getTranslation();
2132+
const glm::mat3 world_to_panel_rotation = glm::transpose(panel_rotation);
2133+
const glm::vec3 view_position = panel_translation;
2134+
const float panel_render_width = static_cast<float>(std::max(panel.render_size.x, 1));
2135+
const float panel_render_height = static_cast<float>(std::max(panel.render_size.y, 1));
2136+
const float panel_cx = panel_render_width * 0.5f;
2137+
const float panel_cy = panel_render_height * 0.5f;
2138+
const auto [panel_fx, panel_fy] =
2139+
lfs::rendering::computePixelFocalLengths(panel.render_size, settings.focal_length_mm);
2140+
const auto project_panel_point = [&](const glm::vec3& world) -> std::optional<glm::vec2> {
2141+
const glm::vec3 view = world_to_panel_rotation * (world - panel_translation);
2142+
if (settings.equirectangular) {
2143+
const float len = glm::length(view);
2144+
if (!std::isfinite(len) || len <= 1e-6f) {
2145+
return std::nullopt;
2146+
}
2147+
const glm::vec3 dir = view / len;
2148+
const float ndc_x = std::atan2(dir.x, -dir.z) / glm::pi<float>();
2149+
const float ndc_y = -std::asin(std::clamp(dir.y, -1.0f, 1.0f)) /
2150+
(glm::pi<float>() * 0.5f);
2151+
if (!std::isfinite(ndc_x) || !std::isfinite(ndc_y)) {
2152+
return std::nullopt;
2153+
}
2154+
return panel.pos + glm::vec2((ndc_x * 0.5f + 0.5f) * panel.size.x,
2155+
(ndc_y * 0.5f + 0.5f) * panel.size.y);
2156+
}
2157+
2158+
constexpr float kMinViewZ = -1e-4f;
2159+
if (view.z >= kMinViewZ) {
2160+
return std::nullopt;
2161+
}
2162+
if (settings.orthographic) {
2163+
if (!std::isfinite(settings.ortho_scale) || settings.ortho_scale <= 0.0f) {
2164+
return std::nullopt;
2165+
}
2166+
return renderToPanelScreen(panel, glm::vec2(panel_cx + view.x * settings.ortho_scale,
2167+
panel_cy - view.y * settings.ortho_scale));
2168+
}
2169+
2170+
const float depth = -view.z;
2171+
if (depth <= 0.0f) {
2172+
return std::nullopt;
2173+
}
2174+
return renderToPanelScreen(panel, glm::vec2(panel_cx + view.x * panel_fx / depth,
2175+
panel_cy - view.y * panel_fy / depth));
2176+
};
22222177
size_t background_thumbnail_requests = 0;
22232178
constexpr size_t kBackgroundThumbnailRequestsPerFrame = 16;
2179+
const std::uint32_t frustum_first_instance =
2180+
static_cast<std::uint32_t>(params.frustum_instances.size());
2181+
params.frustum_instances.reserve(params.frustum_instances.size() + cameras.size());
22242182
for (size_t i = 0; i < cameras.size(); ++i) {
22252183
const auto& camera = cameras[i];
22262184
if (!camera) {
@@ -2274,18 +2232,16 @@ namespace lfs::vis::gui {
22742232
std::array<glm::vec2, image_corners.size()> screen_points{};
22752233
std::array<float, image_corners.size()> corner_depths{};
22762234
bool quad_visible = true;
2277-
const glm::mat3 panel_rotation = panel.viewport->getRotationMatrix();
2278-
const glm::vec3 panel_translation = panel.viewport->getTranslation();
22792235
for (size_t corner = 0; corner < image_corners.size(); ++corner) {
22802236
const glm::vec3 world_point =
22812237
glm::vec3((*model) * glm::vec4(image_corners[corner], 1.0f));
2282-
const auto projected = projectPointToPanelScreen(panel, settings, world_point);
2238+
const auto projected = project_panel_point(world_point);
22832239
if (!projected) {
22842240
quad_visible = false;
22852241
break;
22862242
}
22872243
screen_points[corner] = *projected;
2288-
const glm::vec3 view = glm::transpose(panel_rotation) * (world_point - panel_translation);
2244+
const glm::vec3 view = world_to_panel_rotation * (world_point - panel_translation);
22892245
corner_depths[corner] = settings.equirectangular ? glm::length(view) : -view.z;
22902246
}
22912247
quad_visible = quad_visible && projectedQuadVisible(screen_points, panel);
@@ -2310,10 +2266,32 @@ namespace lfs::vis::gui {
23102266
{emphasis_mix, disabled_mix, 0.0f, 0.0f},
23112267
corner_depths);
23122268
}
2313-
appendPerspectiveCameraFrustum(params, panel, settings, *model, color);
2269+
params.frustum_instances.push_back(VulkanViewportFrustumInstance{
2270+
.model = *model,
2271+
.color = color,
2272+
});
23142273
}
23152274
}
23162275

2276+
const std::uint32_t frustum_instance_count =
2277+
static_cast<std::uint32_t>(params.frustum_instances.size()) - frustum_first_instance;
2278+
if (frustum_instance_count > 0) {
2279+
const glm::mat4 frustum_view =
2280+
lfs::rendering::makeViewMatrix(panel_rotation, panel_translation);
2281+
params.frustum_batches.push_back(VulkanViewportFrustumBatch{
2282+
.view = frustum_view,
2283+
.viewport_pos = panel.pos,
2284+
.viewport_size = panel.size,
2285+
.render_size = glm::vec2(panel.render_size),
2286+
.focal_x = settings.orthographic ? settings.ortho_scale : panel_fx,
2287+
.focal_y = settings.orthographic ? settings.ortho_scale : panel_fy,
2288+
.orthographic = settings.orthographic,
2289+
.equirectangular = settings.equirectangular,
2290+
.first_instance = frustum_first_instance,
2291+
.instance_count = frustum_instance_count,
2292+
});
2293+
}
2294+
23172295
if (thumbnail_cache.hasPendingWork()) {
23182296
rendering_manager.markDirty(DirtyFlag::OVERLAY);
23192297
}

0 commit comments

Comments
 (0)