QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
Viewer3DCameraController.cc
Go to the documentation of this file.
1/****************************************************************************
2 *
3 * (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 *
5 * QGroundControl is licensed according to the terms in the file
6 * COPYING.md in the root of the source code directory.
7 *
8 ****************************************************************************/
9
11
12#include <QtCore/QtMath>
13#include <QtGui/QQuaternion>
14#include <algorithm>
15#include <cmath>
16
17namespace {
18
19constexpr qreal kYawFullDragDegrees = 360.0;
20constexpr qreal kTiltFullDragDegrees = 180.0;
21constexpr qreal kZoomSensitivity = 0.001;
22
24QVector3D centerToCameraDirection(qreal tilt, qreal heading)
25{
26 const qreal tiltRad = qDegreesToRadians(tilt);
27 const qreal headingRad = qDegreesToRadians(heading);
28 return QVector3D(std::sin(tiltRad) * std::sin(headingRad), -std::sin(tiltRad) * std::cos(headingRad),
29 std::cos(tiltRad));
30}
31
32} // namespace
33
34Viewer3DCameraController::Viewer3DCameraController(QObject* parent) : QObject(parent) {}
35
36void Viewer3DCameraController::setOrbitCenter(const QVector3D& center)
37{
38 if (center == _orbitCenter) {
39 return;
40 }
41 _orbitCenter = center;
42 emit orbitCenterChanged();
43}
44
46{
47 if (heading == _heading) {
48 return;
49 }
50 _heading = heading;
51 emit headingChanged();
52}
53
55{
56 tilt = std::clamp(tilt, kMinTilt, kMaxTilt);
57 if (tilt == _tilt) {
58 return;
59 }
60 _tilt = tilt;
61 emit tiltChanged();
62}
63
65{
67 if (distance == _distance) {
68 return;
69 }
70 _distance = distance;
71 emit distanceChanged();
72}
73
75{
76 if (size == _viewportSize) {
77 return;
78 }
79 _viewportSize = size;
81}
82
84{
85 if (fov == _fieldOfView) {
86 return;
87 }
88 _fieldOfView = fov;
89 emit fieldOfViewChanged();
90}
91
93{
94 lookAt(QVector3D(), 0.0, 0.0, kDefaultDistance);
95}
96
97void Viewer3DCameraController::lookAt(const QVector3D& center, qreal heading, qreal tilt, qreal distance)
98{
99 setOrbitCenter(center);
101 setTilt(tilt);
103}
104
105void Viewer3DCameraController::beginPan(const QPointF& screenPos)
106{
107 const auto ground = screenToGround(screenPos);
108 _panAnchorValid = ground.has_value();
109 if (_panAnchorValid) {
110 _panAnchor = *ground;
111 }
112}
113
114void Viewer3DCameraController::panTo(const QPointF& screenPos)
115{
116 if (!_panAnchorValid) {
117 return;
118 }
119 const auto ground = screenToGround(screenPos);
120 if (!ground.has_value()) {
121 return;
122 }
123 // Translate so the anchor returns under the cursor. Ground points move with
124 // the camera, so the required translation is exact in a single step.
125 setOrbitCenter(_orbitCenter + (_panAnchor - *ground));
126}
127
128void Viewer3DCameraController::beginOrbit(const QPointF& screenPos)
129{
130 if (!_viewportSize.isValid() || _viewportSize.isEmpty()) {
131 _orbitAnchorValid = false;
132 return;
133 }
134 const auto ground = screenToGround(screenPos);
135 // When the pick ray misses the ground, orbit about the current center instead
136 _orbitAnchor = ground.value_or(_orbitCenter);
137 _orbitAnchorValid = true;
138 _lastOrbitPos = screenPos;
139}
140
141void Viewer3DCameraController::orbitTo(const QPointF& screenPos)
142{
143 if (!_orbitAnchorValid) {
144 return;
145 }
146
147 const QPointF delta = screenPos - _lastOrbitPos;
148 _lastOrbitPos = screenPos;
149
150 const qreal headingDelta = (delta.x() / _viewportSize.width()) * kYawFullDragDegrees;
151 const qreal tiltDelta = -((delta.y() / _viewportSize.height()) * kTiltFullDragDegrees);
152
153 _rotateAboutAnchor(_orbitAnchor, headingDelta, tiltDelta);
154}
155
156void Viewer3DCameraController::rotateBy(qreal degrees, const QPointF& screenPos)
157{
158 if (!_viewportSize.isValid() || _viewportSize.isEmpty()) {
159 return;
160 }
161 // When the pick ray misses the ground, rotate about the current center instead
162 const QVector3D anchor = screenToGround(screenPos).value_or(_orbitCenter);
163 _rotateAboutAnchor(anchor, degrees, 0.0);
164}
165
166void Viewer3DCameraController::tiltBy(qreal degrees, const QPointF& screenPos)
167{
168 if (!_viewportSize.isValid() || _viewportSize.isEmpty()) {
169 return;
170 }
171 const QVector3D anchor = screenToGround(screenPos).value_or(_orbitCenter);
172 _rotateAboutAnchor(anchor, 0.0, degrees);
173}
174
175void Viewer3DCameraController::_rotateAboutAnchor(const QVector3D& anchor, qreal headingDelta, qreal tiltDelta)
176{
177 tiltDelta = std::clamp(_tilt + tiltDelta, kMinTilt, kMaxTilt) - _tilt;
178
179 QVector3D center = _orbitCenter;
180
181 // Yaw: rigid rotation about the world-z axis through the anchor
182 if (headingDelta != 0.0) {
183 const QQuaternion yaw = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), static_cast<float>(headingDelta));
184 center = anchor + yaw.rotatedVector(center - anchor);
185 setHeading(_heading + headingDelta);
186 }
187
188 // Pitch: rigid rotation about the camera-right axis through the anchor.
189 // Rotating by the tilt delta about right = (cos h, sin h, 0) advances the
190 // center-to-camera direction from u(tilt) to u(tilt + delta).
191 if (tiltDelta != 0.0) {
192 const qreal headingRad = qDegreesToRadians(_heading);
193 const QVector3D rightAxis(static_cast<float>(std::cos(headingRad)), static_cast<float>(std::sin(headingRad)),
194 0.0f);
195 const QQuaternion pitch = QQuaternion::fromAxisAndAngle(rightAxis, static_cast<float>(tiltDelta));
196 center = anchor + pitch.rotatedVector(center - anchor);
197 setTilt(_tilt + tiltDelta);
198 }
199
200 setOrbitCenter(center);
201}
202
203void Viewer3DCameraController::zoom(qreal amount, const QPointF& screenPos)
204{
205 if (!_viewportSize.isValid() || _viewportSize.isEmpty()) {
206 return;
207 }
208
209 // Guard exp() underflow for very large zoom amounts: clamp to a tiny
210 // positive factor so zoomBy still clamps the distance to the minimum.
211 const qreal factor = std::max(std::exp(-amount * kZoomSensitivity), kMinDistance / kMaxDistance);
212 zoomBy(factor, screenPos);
213}
214
215void Viewer3DCameraController::zoomBy(qreal factor, const QPointF& screenPos)
216{
217 if ((factor <= 0.0) || !_viewportSize.isValid() || _viewportSize.isEmpty()) {
218 return;
219 }
220
221 const qreal newDistance = std::clamp(_distance * factor, kMinDistance, kMaxDistance);
222 factor = newDistance / _distance;
223 if (factor == 1.0) {
224 return;
225 }
226
227 const auto ground = screenToGround(screenPos);
228 if (ground.has_value()) {
229 // Move toward/away from the point under the cursor so it stays put on screen
230 setOrbitCenter(*ground + ((_orbitCenter - *ground) * static_cast<float>(factor)));
231 }
232 setDistance(newDistance);
233}
234
236{
237 if (!_viewportSize.isValid() || _viewportSize.isEmpty()) {
238 return 0.0;
239 }
240 // Ground span per screen-x pixel at screen center. The camera-right axis is
241 // ground-parallel, so this is tilt-independent.
242 const qreal tanHalfFov = std::tan(qDegreesToRadians(_fieldOfView / 2.0));
243 return (2.0 * _distance * tanHalfFov) / _viewportSize.height();
244}
245
247{
248 return _orbitCenter + (centerToCameraDirection(_tilt, _heading) * static_cast<float>(_distance));
249}
250
251std::optional<QVector3D> Viewer3DCameraController::screenToGround(const QPointF& screenPos) const
252{
253 if (!_viewportSize.isValid() || _viewportSize.isEmpty()) {
254 return std::nullopt;
255 }
256
257 const qreal tiltRad = qDegreesToRadians(_tilt);
258 const qreal headingRad = qDegreesToRadians(_heading);
259 const qreal sinT = std::sin(tiltRad);
260 const qreal cosT = std::cos(tiltRad);
261 const qreal sinH = std::sin(headingRad);
262 const qreal cosH = std::cos(headingRad);
263
264 // Camera basis in scene space (z-up)
265 const QVector3D forward(static_cast<float>(-sinT * sinH), static_cast<float>(sinT * cosH),
266 static_cast<float>(-cosT));
267 const QVector3D right(static_cast<float>(cosH), static_cast<float>(sinH), 0.0f);
268 const QVector3D up(static_cast<float>(-sinH * cosT), static_cast<float>(cosH * cosT), static_cast<float>(sinT));
269
270 // Pick ray direction through the pixel (vertical field of view)
271 const qreal tanHalfFov = std::tan(qDegreesToRadians(_fieldOfView / 2.0));
272 const qreal aspect = _viewportSize.width() / _viewportSize.height();
273 const qreal ndcX = ((2.0 * screenPos.x()) / _viewportSize.width()) - 1.0;
274 const qreal ndcY = 1.0 - ((2.0 * screenPos.y()) / _viewportSize.height());
275 const QVector3D rayDir = (right * static_cast<float>(ndcX * tanHalfFov * aspect)) +
276 (up * static_cast<float>(ndcY * tanHalfFov)) + forward;
277
278 if (rayDir.z() >= 0.0f) {
279 return std::nullopt;
280 }
281
282 const QVector3D rayOrigin = cameraPosition();
283 const float s = -rayOrigin.z() / rayDir.z();
284 if (s <= 0.0f) {
285 return std::nullopt;
286 }
287 return rayOrigin + (rayDir * s);
288}
Q_INVOKABLE void beginOrbit(const QPointF &screenPos)
Start an orbit gesture at the given screen position (pixels)
Q_INVOKABLE void zoomBy(qreal factor, const QPointF &screenPos)
void setOrbitCenter(const QVector3D &center)
std::optional< QVector3D > screenToGround(const QPointF &screenPos) const
Q_INVOKABLE void orbitTo(const QPointF &screenPos)
Viewer3DCameraController(QObject *parent=nullptr)
Q_INVOKABLE void lookAt(const QVector3D &center, qreal heading, qreal tilt, qreal distance)
Set the full pose in one call. tilt/distance are clamped.
static constexpr qreal kMinTilt
Q_INVOKABLE void rotateBy(qreal degrees, const QPointF &screenPos)
static constexpr qreal kDefaultDistance
Q_INVOKABLE void zoom(qreal amount, const QPointF &screenPos)
static constexpr qreal kMaxDistance
Q_INVOKABLE void reset()
Restore the default pose (center origin, heading 0, tilt 0, default distance)
Q_INVOKABLE void beginPan(const QPointF &screenPos)
Start a pan gesture at the given screen position (pixels)
static constexpr qreal kMinDistance
Q_INVOKABLE void panTo(const QPointF &screenPos)
Continue a pan gesture: the ground point picked at beginPan stays under the cursor.
static constexpr qreal kMaxTilt
QVector3D cameraPosition() const
Derived camera position in scene space.
void setViewportSize(const QSizeF &size)
Q_INVOKABLE void tiltBy(qreal degrees, const QPointF &screenPos)
Q_INVOKABLE qreal sceneUnitsPerPixel() const