Skip to main content

CameraWidget.ixx File

ImGui widget for controlling and configuring camera parameters. More...

Included Headers

#include <memory> #include <string> #include <vector> #include "imgui.h" #include <helios.math.utils> #include <helios.core.spatial.Transform> #include <helios.math.types> #include <helios.scene.CameraSceneNode> #include <helios.scene.Camera> #include <helios.ext.imgui.ImGuiWidget>

Namespaces Index

namespacehelios
namespaceext

Platform-specific extensions and backend implementations. More...

namespaceimgui
namespacewidgets

Debug and developer widgets for ImGui overlays. More...

Classes Index

classCameraWidget

Debug widget for real-time camera parameter control and visualization. More...

structCameraEntry

Description

ImGui widget for controlling and configuring camera parameters.

File Listing

The file content with the documentation metadata removed is:

1/**
2 * @file CameraWidget.ixx
3 * @brief ImGui widget for controlling and configuring camera parameters.
4 */
5module;
6
7#include <memory>
8#include <string>
9#include <vector>
10#include "imgui.h"
11
12export module helios.ext.imgui.widgets.CameraWidget;
13
14import helios.ext.imgui.ImGuiWidget;
15import helios.scene.Camera;
16import helios.scene.CameraSceneNode;
17import helios.core.spatial.Transform;
18import helios.math.types;
19import helios.math.utils;
20
22
23 /**
24 * @class CameraWidget
25 * @brief Debug widget for real-time camera parameter control and visualization.
26 */
27 class CameraWidget : public ImGuiWidget {
28
29 private:
30 struct CameraEntry {
31 std::string name;
32 helios::scene::CameraSceneNode* node = nullptr;
33
34 helios::math::vec3f initialTranslation{0.0f, 0.0f, 5.0f};
35 helios::math::vec3f initialScale{1.0f, 1.0f, 1.0f};
37
38 float initialFovDegrees = 90.0f;
39 float initialAspectRatio = 16.0f / 9.0f;
40 float initialZNear = 0.1f;
41 float initialZFar = 1000.0f;
42
43 helios::math::vec3f initialLookAtTarget{0.0f, 0.0f, 0.0f};
44 helios::math::vec3f initialUp{0.0f, 1.0f, 0.0f};
45 };
46
47 enum class LookAtSpace { Local, World };
48
49 std::vector<CameraEntry> cameras_;
50 int selectedCameraIndex_ = 0;
51
52 // Temporary UI values
53 helios::math::vec3f tempTranslation_{0.0f, 0.0f, 5.0f};
54 helios::math::vec3f tempLookAtTarget_{0.0f, 0.0f, 0.0f};
55 helios::math::vec3f tempUp_{0.0f, 1.0f, 0.0f};
56
57 float tempFovDegrees_ = 90.0f;
58 float tempAspectRatio_ = 16.0f / 9.0f;
59 float tempZNear_ = 0.1f;
60 float tempZFar_ = 1000.0f;
61
62 // LookAt mode
63 LookAtSpace lookAtSpace_ = LookAtSpace::Local;
64
65 // Apply behavior toggles
66 bool applyTranslationOnChange_ = true;
67 bool applyLookAtOnChange_ = true;
68 bool applyBothOnAnyChange_ = false; // if enabled, apply both whenever either changes
69
70 [[nodiscard]] helios::scene::CameraSceneNode* getCurrentCameraNode() noexcept {
71 if (cameras_.empty()) {
72 return nullptr;
73 }
74 if (selectedCameraIndex_ < 0 || selectedCameraIndex_ >= static_cast<int>(cameras_.size())) {
75 selectedCameraIndex_ = 0;
76 }
77 return cameras_[selectedCameraIndex_].node;
78 }
79
80 [[nodiscard]] helios::scene::Camera* getActiveCamera() noexcept {
81 auto* node = getCurrentCameraNode();
82 if (!node) {
83 return nullptr;
84 }
85 return &(node->camera());
86 }
87
88 /**
89 * Applies translation and/or lookAt depending on change flags and apply-mode toggles.
90 */
91 void applyTransformToNode(helios::scene::CameraSceneNode* node,
92 bool translationChanged,
93 bool lookAtChanged)
94 {
95 if (!node) return;
96
97 bool applyTranslation = applyTranslationOnChange_ && translationChanged;
98 bool applyLookAt = applyLookAtOnChange_ && lookAtChanged;
99
100 // Force both together if enabled
101 if (applyBothOnAnyChange_ && (translationChanged || lookAtChanged)) {
102 applyTranslation = true;
103 applyLookAt = true;
104 }
105
106 if (applyTranslation) {
107 node->setTranslation(tempTranslation_);
108 }
109
110 if (!applyLookAt) {
111 return;
112 }
113
114 if (lookAtSpace_ == LookAtSpace::Local) {
115 node->lookAtLocal(tempLookAtTarget_, tempUp_);
116 } else {
117 node->lookAt(tempLookAtTarget_, tempUp_);
118 }
119 }
120
121 void syncTempValuesFromCamera() noexcept {
122 auto* node = getCurrentCameraNode();
123 if (!node) {
124 return;
125 }
126
127 const auto& transform = node->localTransform();
128 tempTranslation_ = transform.translation();
129
130 const auto& cam = node->camera();
131 tempFovDegrees_ = helios::math::degrees(cam.fovY());
132 tempAspectRatio_ = cam.aspectRatio();
133 tempZNear_ = cam.zNear();
134 tempZFar_ = cam.zFar();
135 }
136
137 void captureInitialValues(CameraEntry& entry) noexcept {
138 if (!entry.node) {
139 return;
140 }
141
142 const auto& transform = entry.node->localTransform();
143 entry.initialTranslation = transform.translation();
144 entry.initialScale = transform.scaling();
145 entry.initialRotation = transform.rotation();
146
147 const auto& cam = entry.node->camera();
148 entry.initialFovDegrees = helios::math::degrees(cam.fovY());
149 entry.initialAspectRatio = cam.aspectRatio();
150 entry.initialZNear = cam.zNear();
151 entry.initialZFar = cam.zFar();
152 }
153
154 void resetToInitialValues() noexcept {
155 if (cameras_.empty()) {
156 return;
157 }
158
159 auto& entry = cameras_[selectedCameraIndex_];
160 auto* node = entry.node;
161 if (!node) {
162 return;
163 }
164
165 tempTranslation_ = entry.initialTranslation;
166 tempLookAtTarget_ = entry.initialLookAtTarget;
167 tempUp_ = entry.initialUp;
168
169 node->setTranslation(entry.initialTranslation);
170 node->setRotation(entry.initialRotation);
171 node->setScale(entry.initialScale);
172
173 auto& cam = node->camera();
174 cam.setPerspective(helios::math::radians(entry.initialFovDegrees),
175 entry.initialAspectRatio,
176 entry.initialZNear,
177 entry.initialZFar);
178
179 // Apply lookAt immediately after reset if user wants to keep that behavior consistent
180 // (Here we always do it because reset expects the camera to match initial pose.)
181 if (lookAtSpace_ == LookAtSpace::Local) {
182 node->lookAtLocal(tempLookAtTarget_, tempUp_);
183 } else {
184 node->lookAt(tempLookAtTarget_, tempUp_);
185 }
186
187 syncTempValuesFromCamera();
188 }
189
190 public:
191 CameraWidget() = default;
192
193 void addCameraSceneNode(const std::string& name, helios::scene::CameraSceneNode* node) {
194 CameraEntry entry{name, node};
195 captureInitialValues(entry);
196
197 // store current temp defaults as initial values for look-at
198 entry.initialLookAtTarget = tempLookAtTarget_;
199 entry.initialUp = tempUp_;
200
201 cameras_.push_back(entry);
202
203 if (cameras_.size() == 1) {
204 syncTempValuesFromCamera();
205 }
206 }
207
208 void clearCameras() noexcept {
209 cameras_.clear();
210 selectedCameraIndex_ = 0;
211 }
212
213 void draw() override {
214 ImGui::SetNextWindowSize(ImVec2(320, 620), ImGuiCond_FirstUseEver);
215
216 if (!ImGui::Begin("Camera Control", nullptr, ImGuiWindowFlags_NoCollapse)) {
217 ImGui::End();
218 return;
219 }
220
221 if (cameras_.empty()) {
222 ImGui::TextDisabled("No cameras registered.");
223 ImGui::End();
224 return;
225 }
226
227 // Camera selection
228 ImGui::PushItemWidth(-100);
229 if (ImGui::BeginCombo("##Camera", cameras_[selectedCameraIndex_].name.c_str())) {
230 for (int i = 0; i < static_cast<int>(cameras_.size()); ++i) {
231 const bool isSelected = (selectedCameraIndex_ == i);
232 if (ImGui::Selectable(cameras_[i].name.c_str(), isSelected)) {
233 selectedCameraIndex_ = i;
234 syncTempValuesFromCamera();
235 }
236 if (isSelected) {
237 ImGui::SetItemDefaultFocus();
238 }
239 }
240 ImGui::EndCombo();
241 }
242 ImGui::PopItemWidth();
243
244 ImGui::SameLine();
245 if (ImGui::Button("Reset")) {
246 resetToInitialValues();
247 }
248
249 auto* node = getCurrentCameraNode();
250 auto* cameraPtr = getActiveCamera();
251 if (!node || !cameraPtr) {
252 ImGui::End();
253 return;
254 }
255 auto& camera = *cameraPtr;
256
257 ImGui::Separator();
258 ImGui::Spacing();
259
260 bool translationChanged = false;
261 bool lookAtChanged = false;
262
263 // === Transform ===
264 ImGui::Text("Position (Translation)");
265 translationChanged |= ImGui::DragFloat("X##Pos", &tempTranslation_[0], 0.1f, -100.0f, 100.0f, "%.2f");
266 translationChanged |= ImGui::DragFloat("Y##Pos", &tempTranslation_[1], 0.1f, -100.0f, 100.0f, "%.2f");
267 translationChanged |= ImGui::DragFloat("Z##Pos", &tempTranslation_[2], 0.1f, -100.0f, 100.0f, "%.2f");
268
269 ImGui::Spacing();
270
271 // LookAt space toggle
272 ImGui::Text("LookAt Space");
273 const int mode = (lookAtSpace_ == LookAtSpace::Local) ? 0 : 1;
274
275 if (ImGui::RadioButton("Local (parent/model space)", mode == 0)) {
276 lookAtSpace_ = LookAtSpace::Local;
277 lookAtChanged = true;
278 }
279 ImGui::SameLine();
280 if (ImGui::RadioButton("World", mode == 1)) {
281 lookAtSpace_ = LookAtSpace::World;
282 lookAtChanged = true;
283 }
284
285 ImGui::Spacing();
286
287 // Apply behavior toggles (horizontal layout for compactness)
288 ImGui::Separator();
289 ImGui::Text("Apply Behavior");
290 ImGui::Checkbox("Translation", &applyTranslationOnChange_);
291 ImGui::SameLine();
292 ImGui::Checkbox("LookAt", &applyLookAtOnChange_);
293 ImGui::SameLine();
294 ImGui::Checkbox("Both", &applyBothOnAnyChange_);
295 if (ImGui::IsItemHovered()) {
296 ImGui::SetTooltip("If enabled, changing either Position or LookAt re-applies BOTH.");
297 }
298
299 ImGui::Separator();
300 ImGui::Spacing();
301
302 // LookAt target controls
303 ImGui::Text("Look-At Target");
304 lookAtChanged |= ImGui::DragFloat("X##Target", &tempLookAtTarget_[0], 0.1f, -100.0f, 100.0f, "%.2f");
305 lookAtChanged |= ImGui::DragFloat("Y##Target", &tempLookAtTarget_[1], 0.1f, -100.0f, 100.0f, "%.2f");
306 lookAtChanged |= ImGui::DragFloat("Z##Target", &tempLookAtTarget_[2], 0.1f, -100.0f, 100.0f, "%.2f");
307
308 ImGui::Spacing();
309
310 ImGui::Text("Up Vector");
311 lookAtChanged |= ImGui::DragFloat("X##Up", &tempUp_[0], 0.01f, -1.0f, 1.0f, "%.3f");
312 lookAtChanged |= ImGui::DragFloat("Y##Up", &tempUp_[1], 0.01f, -1.0f, 1.0f, "%.3f");
313 lookAtChanged |= ImGui::DragFloat("Z##Up", &tempUp_[2], 0.01f, -1.0f, 1.0f, "%.3f");
314
315 ImGui::SameLine();
316 if (ImGui::SmallButton("N##NormalizeUp")) {
317 float len = tempUp_.length();
318 if (len > 0.0001f) {
319 tempUp_ = tempUp_.normalize();
320 lookAtChanged = true;
321 }
322 }
323 if (ImGui::IsItemHovered()) {
324 ImGui::SetTooltip("Normalize up vector");
325 }
326
327 // Apply changes respecting apply-mode toggles
328 if (translationChanged || lookAtChanged) {
329 applyTransformToNode(node, translationChanged, lookAtChanged);
330 }
331
332 ImGui::Separator();
333 ImGui::Spacing();
334
335 // === Projection ===
336 ImGui::Text("Projection");
337
338 bool projectionChanged = false;
339 projectionChanged |= ImGui::SliderFloat("FOV", &tempFovDegrees_, 30.0f, 120.0f, "%.1f deg");
340 projectionChanged |= ImGui::DragFloat("Near Plane", &tempZNear_, 0.01f, 0.001f, tempZFar_ - 0.01f, "%.3f");
341 projectionChanged |= ImGui::DragFloat("Far Plane", &tempZFar_, 1.0f, tempZNear_ + 0.01f, 100000.0f, "%.1f");
342
343 if (projectionChanged) {
344 camera.setPerspective(helios::math::radians(tempFovDegrees_), tempAspectRatio_, tempZNear_, tempZFar_);
345 }
346
347 ImGui::Spacing();
348
349 ImGui::Text("Aspect Ratio");
350 if (ImGui::DragFloat("##Aspect", &tempAspectRatio_, 0.01f, 0.5f, 4.0f, "%.3f")) {
351 camera.setAspectRatio(tempAspectRatio_);
352 }
353
354 if (ImGui::Button("16:9")) {
355 tempAspectRatio_ = 16.0f / 9.0f;
356 camera.setAspectRatio(tempAspectRatio_);
357 }
358 ImGui::SameLine();
359 if (ImGui::Button("21:9")) {
360 tempAspectRatio_ = 21.0f / 9.0f;
361 camera.setAspectRatio(tempAspectRatio_);
362 }
363 ImGui::SameLine();
364 if (ImGui::Button("32:9")) {
365 tempAspectRatio_ = 32.0f / 9.0f;
366 camera.setAspectRatio(tempAspectRatio_);
367 }
368 ImGui::SameLine();
369 if (ImGui::Button("4:3")) {
370 tempAspectRatio_ = 4.0f / 3.0f;
371 camera.setAspectRatio(tempAspectRatio_);
372 }
373 ImGui::SameLine();
374 if (ImGui::Button("1:1")) {
375 tempAspectRatio_ = 1.0f;
376 camera.setAspectRatio(tempAspectRatio_);
377 }
378
379 ImGui::Separator();
380 ImGui::Spacing();
381
382 // Quick view presets
383 ImGui::Text("Quick View Presets");
384 if (ImGui::Button("Front")) {
385 tempTranslation_ = {0.0f, 0.0f, 5.0f};
386 tempLookAtTarget_ = {0.0f, 0.0f, 0.0f};
387 tempUp_ = {0.0f, 1.0f, 0.0f};
388 applyTransformToNode(node, true, true);
389 }
390 ImGui::SameLine();
391 if (ImGui::Button("Top")) {
392 tempTranslation_ = {0.0f, 5.0f, 0.001f};
393 tempLookAtTarget_ = {0.0f, 0.0f, 0.0f};
394 tempUp_ = {0.0f, 0.0f, -1.0f};
395 applyTransformToNode(node, true, true);
396 }
397 ImGui::SameLine();
398 if (ImGui::Button("Side")) {
399 tempTranslation_ = {5.0f, 0.0f, 0.0f};
400 tempLookAtTarget_ = {0.0f, 0.0f, 0.0f};
401 tempUp_ = {0.0f, 1.0f, 0.0f};
402 applyTransformToNode(node, true, true);
403 }
404 ImGui::SameLine();
405 if (ImGui::Button("Iso")) {
406 tempTranslation_ = {5.0f, 5.0f, 5.0f};
407 tempLookAtTarget_ = {0.0f, 0.0f, 0.0f};
408 tempUp_ = {0.0f, 1.0f, 0.0f};
409 applyTransformToNode(node, true, true);
410 }
411
412 ImGui::Separator();
413
414 helios::math::vec3f diff = tempTranslation_ - tempLookAtTarget_;
415 float distance = diff.length();
416
417 ImGui::TextDisabled("Pos: (%.1f, %.1f, %.1f) | Target: (%.1f, %.1f, %.1f)",
418 tempTranslation_[0], tempTranslation_[1], tempTranslation_[2],
419 tempLookAtTarget_[0], tempLookAtTarget_[1], tempLookAtTarget_[2]);
420 ImGui::TextDisabled("Distance: %.2f | FOV: %.0f° | Z: [%.2f, %.0f]",
421 distance, tempFovDegrees_, tempZNear_, tempZFar_);
422
423 ImGui::End();
424 }
425 };
426
427} // namespace helios::ext::imgui::widgets

Generated via doxygen2docusaurus 2.0.0 by Doxygen 1.15.0.