1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
4#include "qquick3dcamera_p.h"
5
6#include <QtQuick3DRuntimeRender/private/qssgrendercamera_p.h>
7
8#include "qquick3dquaternionutils_p.h"
9#include "qquick3dnode_p_p.h"
10
11#include <QtMath>
12#include <QtQuick3DUtils/private/qssgutils_p.h>
13
14#include "qquick3dutils_p.h"
15
16QT_BEGIN_NAMESPACE
17
18/*!
19 \qmltype Camera
20 \inherits Node
21 \inqmlmodule QtQuick3D
22 \brief Defines an abstract base for Cameras.
23
24 A Camera defines how the content of the 3D scene is projected onto a 2D surface,
25 such as a View3D. A scene needs at least one Camera in order to visualize its
26 contents.
27
28 It is possible to position and rotate the Camera like any other spatial \l{QtQuick3D::Node}{Node} in
29 the scene. The \l{QtQuick3D::Node}{Node}'s location and orientation determines where the Camera is in
30 the scene, and what direction it is facing. The default orientation of the camera
31 has its forward vector pointing along the negative Z axis and its up vector along
32 the positive Y axis.
33
34 Together with the position and orientation, the frustum defines which parts of
35 a scene are visible to the Camera and how they are projected onto the 2D surface.
36 The different Camera subtypes provide multiple options to determine the shape of the
37 Camera's frustum.
38
39 \list
40 \li PerspectiveCamera provides a camera with a pyramid-shaped frustum, where objects are
41 projected so that those further away from the camera appear to be smaller. This is the
42 most commonly used camera type, and corresponds to how most real world cameras work.
43 \li OrthographicCamera provides a camera where the lines of the frustum are parallel,
44 making the perceived scale of an object unaffected by its distance to the camera. Typical
45 use cases for this type of camera are CAD (Computer-Assisted Design) applications and
46 cartography.
47 \li FrustumCamera is a perspective camera type where the frustum can be freely customized
48 by the coordinates of its intersection with the near plane. It can be useful if an
49 asymmetrical camera frustum is needed.
50 \li CustomCamera is a camera type where the projection matrix can be freely customized,
51 and can be useful for advanced users who wish to calculate their own projection matrix.
52 \endlist
53
54 To illustrate the difference, these screenshots show the same scene as projected by a
55 PerspectiveCamera and an OrthographicCamera. Notice how the red box is smaller than the
56 green box in the image rendered using the perspective projection.
57 \table
58 \header
59 \li Perspective camera
60 \li Orthographic camera
61 \row
62 \li \image perspectivecamera.png
63 \li \image orthographiccamera.png
64 \endtable
65
66 \sa {Qt Quick 3D - View3D Example}
67*/
68
69/*!
70 \internal
71*/
72QQuick3DCamera::QQuick3DCamera(QQuick3DNodePrivate &dd, QQuick3DNode *parent)
73 : QQuick3DNode(dd, parent) {}
74
75/*!
76 \qmlproperty bool Camera::frustumCullingEnabled
77
78 When this property is \c true, objects outside the camera frustum will be culled, meaning they will
79 not be passed to the renderer. By default this property is set to \c false. For scenes where all or
80 most objects are inside the camera frustum, frustum culling is an unnecessary performance overhead.
81 But for complex scenes where large parts are located outside the camera's view, enabling frustum
82 culling may improve performance.
83*/
84bool QQuick3DCamera::frustumCullingEnabled() const
85{
86 return m_frustumCullingEnabled;
87}
88
89void QQuick3DCamera::setFrustumCullingEnabled(bool frustumCullingEnabled)
90{
91 if (m_frustumCullingEnabled == frustumCullingEnabled)
92 return;
93
94 m_frustumCullingEnabled = frustumCullingEnabled;
95 emit frustumCullingEnabledChanged();
96 update();
97}
98
99/*!
100 \qmlproperty Node Camera::lookAtNode
101
102 If this property is set to a \c non-null value, the rotation of this camera is automatically
103 updated so that this camera keeps looking at the specified node whenever the scene position of
104 this camera or the specified node changes.
105 By default this property is set to \c{null}.
106
107 \sa lookAt
108*/
109QQuick3DNode *QQuick3DCamera::lookAtNode() const
110{
111 return m_lookAtNode;
112}
113
114void QQuick3DCamera::setLookAtNode(QQuick3DNode *node)
115{
116 if (m_lookAtNode == node)
117 return;
118
119 if (m_lookAtNode) {
120 disconnect(sender: m_lookAtNode, signal: &QQuick3DNode::scenePositionChanged, receiver: this, slot: &QQuick3DCamera::updateLookAt);
121 disconnect(sender: this, signal: &QQuick3DNode::scenePositionChanged, receiver: this, slot: &QQuick3DCamera::updateLookAt);
122 }
123
124 m_lookAtNode = node;
125
126 if (m_lookAtNode) {
127 connect(sender: m_lookAtNode, signal: &QQuick3DNode::scenePositionChanged, context: this, slot: &QQuick3DCamera::updateLookAt);
128 connect(sender: this, signal: &QQuick3DNode::scenePositionChanged, context: this, slot: &QQuick3DCamera::updateLookAt);
129 }
130
131 emit lookAtNodeChanged();
132 updateLookAt();
133}
134
135/*!
136 \qmlmethod vector3d Camera::mapToViewport(vector3d scenePos)
137
138 Transforms \a scenePos from global scene space (3D) into viewport space (2D).
139
140 The returned position is normalized, with the top-left of the viewport
141 at [0, 0] and the bottom-right at [1, 1]. The returned z-value will contain
142 the distance from the near clip plane of the frustum (clipNear) to \a scenePos in
143 scene coordinates. If the distance is negative, the point is behind camera.
144
145 If \a scenePos cannot successfully be mapped to a position in the viewport, a
146 position of [0, 0, 0] is returned.
147
148 \sa mapFromViewport(), {View3D::mapFrom3DScene()}{View3D.mapFrom3DScene()}
149*/
150QVector3D QQuick3DCamera::mapToViewport(const QVector3D &scenePos) const
151{
152 QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(item: this)->spatialNode);
153 if (!cameraNode)
154 return QVector3D(0, 0, 0);
155
156 QVector4D scenePosRightHand(scenePos, 1);
157
158 // Transform position
159 const QMatrix4x4 sceneToCamera = sceneTransform().inverted();
160 const QMatrix4x4 projectionViewMatrix = cameraNode->projection * sceneToCamera;
161 const QVector4D transformedScenePos = QSSGUtils::mat44::transform(m: projectionViewMatrix, v: scenePosRightHand);
162
163 if (qFuzzyIsNull(f: transformedScenePos.w()) || qIsNaN(f: transformedScenePos.w()))
164 return QVector3D(0, 0, 0);
165
166 // Normalize scenePosView between [-1, 1]
167 QVector3D scenePosView = transformedScenePos.toVector3D() / transformedScenePos.w();
168
169 // Set z to be the scene distance from clipNear so that the return value
170 // can be used as argument to viewportToscene() to reverse the call.
171 const QVector4D clipNearPos(scenePosView.x(), scenePosView.y(), -1, 1);
172 auto invProj = projectionViewMatrix.inverted();
173 const QVector4D clipNearPosTransformed = QSSGUtils::mat44::transform(m: invProj, v: clipNearPos);
174 if (qFuzzyIsNull(f: clipNearPosTransformed.w()) || qIsNaN(f: clipNearPosTransformed.w()))
175 return QVector3D(0, 0, 0);
176 const QVector4D clipNearPosScene = clipNearPosTransformed / clipNearPosTransformed.w();
177 QVector4D clipFarPos = clipNearPos;
178 clipFarPos.setZ(0);
179 const QVector4D clipFarPosTransformed = QSSGUtils::mat44::transform(m: invProj, v: clipFarPos);
180 if (qFuzzyIsNull(f: clipFarPosTransformed.w()) || qIsNaN(f: clipFarPosTransformed.w()))
181 return QVector3D(0, 0, 0);
182 const QVector4D clipFarPosScene = clipFarPosTransformed / clipFarPosTransformed.w();
183 const QVector3D direction = (clipFarPosScene - clipNearPosScene).toVector3D();
184 const QVector3D scenePosVec = (scenePosRightHand - clipNearPosScene).toVector3D();
185
186 const float distanceToScenePos = scenePosVec.length()
187 * (QVector3D::dotProduct(v1: direction, v2: scenePosVec) > 0.f ? 1 : -1);
188
189 scenePosView.setZ(distanceToScenePos);
190
191 // Convert x and y to be between [0, 1]
192 scenePosView.setX((scenePosView.x() / 2) + 0.5f);
193 scenePosView.setY((scenePosView.y() / 2) + 0.5f);
194 // And convert origin from bottom-left to top-left
195 scenePosView.setY(1 - scenePosView.y());
196 return scenePosView;
197}
198
199/*!
200 \qmlmethod vector3d Camera::mapFromViewport(vector3d viewportPos)
201
202 Transforms \a viewportPos from viewport space (2D) into global scene space (3D).
203
204 The x- and y-values of \a viewportPos must be normalized, with the top-left
205 of the viewport at [0, 0] and the bottom-right at [1, 1]. The z-value is interpreted
206 as the distance from the near clip plane of the frustum (clipNear).
207
208 If \a viewportPos cannot successfully be mapped to a position in the scene, a position
209 of [0, 0, 0] is returned.
210
211 \sa mapToViewport, {View3D::mapTo3DScene()}{View3D.mapTo3DScene()}
212*/
213QVector3D QQuick3DCamera::mapFromViewport(const QVector3D &viewportPos) const
214{
215 QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(item: this)->spatialNode);
216 if (!cameraNode)
217 return QVector3D(0, 0, 0);
218
219 // Pick two positions in the frustum
220 QVector4D clipNearPos(viewportPos, 1);
221 // Convert origin from top-left to bottom-left
222 clipNearPos.setY(1 - clipNearPos.y());
223 // Convert to homogenous position between [-1, 1]
224 clipNearPos.setX((clipNearPos.x() * 2.0f) - 1.0f);
225 clipNearPos.setY((clipNearPos.y() * 2.0f) - 1.0f);
226 QVector4D clipFarPos = clipNearPos;
227 // clipNear: z = -1, clipFar: z = 1. It's recommended to use 0 as
228 // far pos instead of clipFar because of infinite projection issues.
229 clipNearPos.setZ(-1);
230 clipFarPos.setZ(0);
231
232 // Transform position to scene
233 const QMatrix4x4 sceneToCamera = sceneTransform().inverted();
234 const QMatrix4x4 projectionViewMatrixInv = (cameraNode->projection * sceneToCamera).inverted();
235 const QVector4D transformedClipNearPos = QSSGUtils::mat44::transform(m: projectionViewMatrixInv, v: clipNearPos);
236 const QVector4D transformedClipFarPos = QSSGUtils::mat44::transform(m: projectionViewMatrixInv, v: clipFarPos);
237
238 if (qFuzzyIsNull(f: transformedClipNearPos.w()) || qIsNaN(f: transformedClipNearPos.w()) ||
239 qFuzzyIsNull(f: transformedClipFarPos.w()) || qIsNaN(f: transformedClipFarPos.w()))
240 return QVector3D(0, 0, 0);
241
242 // Reverse the projection
243 const QVector3D clipNearPosScene = transformedClipNearPos.toVector3D()
244 / transformedClipNearPos.w();
245 const QVector3D clipFarPosScene = transformedClipFarPos.toVector3D()
246 / transformedClipFarPos.w();
247
248 // Calculate the position in the scene
249 const QVector3D direction = (clipFarPosScene - clipNearPosScene).normalized();
250 const float distanceFromClipNear = viewportPos.z();
251 QVector3D scenePos = clipNearPosScene + (direction * distanceFromClipNear);
252
253 return scenePos;
254}
255
256/*!
257 * \internal
258 */
259QVector3D QQuick3DCamera::mapToViewport(const QVector3D &scenePos,
260 qreal width,
261 qreal height)
262{
263 // We can be called before a spatial node is created, if that is the case, create the node now.
264 if (QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(updateSpatialNode(node: QQuick3DObjectPrivate::get(item: this)->spatialNode))) {
265 QQuick3DObjectPrivate::get(item: this)->spatialNode = cameraNode;
266 cameraNode->calculateGlobalVariables(inViewport: QRect(0, 0, width * cameraNode->dpr, height * cameraNode->dpr));
267 }
268
269 return QQuick3DCamera::mapToViewport(scenePos);
270}
271
272/*!
273 * \internal
274 */
275QVector3D QQuick3DCamera::mapFromViewport(const QVector3D &viewportPos,
276 qreal width,
277 qreal height)
278{
279 // We can be called before a spatial node is created, if that is the case, create the node now.
280 if (QSSGRenderCamera *cameraNode = static_cast<QSSGRenderCamera *>(updateSpatialNode(node: QQuick3DObjectPrivate::get(item: this)->spatialNode))) {
281 QQuick3DObjectPrivate::get(item: this)->spatialNode = cameraNode;
282 cameraNode->calculateGlobalVariables(inViewport: QRect(0, 0, width * cameraNode->dpr, height * cameraNode->dpr));
283 }
284
285 return QQuick3DCamera::mapFromViewport(viewportPos);
286}
287
288/*!
289 \qmlmethod vector3d Camera::lookAt(vector3d scenePos)
290 \since 5.15
291
292 Sets the rotation value of the Camera so that it is pointing at \a scenePos.
293*/
294
295void QQuick3DCamera::lookAt(const QVector3D &scenePos)
296{
297 // Assumption: we never want the camera to roll.
298 // We use Euler angles here to avoid roll to sneak in through numerical instability.
299
300 const auto &targetPosition = scenePos;
301 auto sourcePosition = scenePosition();
302
303 QVector3D targetVector = sourcePosition - targetPosition;
304
305 float yaw = qRadiansToDegrees(radians: atan2(y: targetVector.x(), x: targetVector.z()));
306
307 QVector2D p(targetVector.x(), targetVector.z()); // yaw vector projected to horizontal plane
308 float pitch = qRadiansToDegrees(radians: atan2(y: p.length(), x: targetVector.y())) - 90;
309
310 const float previousRoll = eulerRotation().z();
311 setEulerRotation(QVector3D(pitch, yaw, previousRoll));
312}
313
314/*!
315 \qmlmethod vector3d Camera::lookAt(QtQuick3D::Node node)
316 \since 5.15
317
318 Sets the rotation value of the Camera so that it is pointing at \a node.
319*/
320
321void QQuick3DCamera::lookAt(QQuick3DNode *node)
322{
323 if (!node)
324 return;
325 lookAt(scenePos: node->scenePosition());
326}
327
328void QQuick3DCamera::updateGlobalVariables(const QRectF &inViewport)
329{
330 QSSGRenderCamera *node = static_cast<QSSGRenderCamera *>(QQuick3DObjectPrivate::get(item: this)->spatialNode);
331 if (node)
332 node->calculateGlobalVariables(inViewport);
333}
334
335/*!
336 * \internal
337 */
338QSSGRenderGraphObject *QQuick3DCamera::updateSpatialNode(QSSGRenderGraphObject *node)
339{
340 if (!node) {
341 markAllDirty();
342 node = new QSSGRenderCamera(QQuick3DObjectPrivate::get(item: this)->type);
343 }
344
345 QQuick3DNode::updateSpatialNode(node);
346
347 QSSGRenderCamera *camera = static_cast<QSSGRenderCamera *>(node);
348 if (qUpdateIfNeeded(orig&: camera->enableFrustumClipping, updated: m_frustumCullingEnabled))
349 camera->markDirty(dirtyFlag: QSSGRenderCamera::DirtyFlag::CameraDirty);
350 if (qUpdateIfNeeded(orig&: camera->levelOfDetailPixelThreshold, updated: m_levelOfDetailBias))
351 camera->markDirty(dirtyFlag: QSSGRenderCamera::DirtyFlag::CameraDirty);
352
353 return node;
354}
355
356void QQuick3DCamera::updateLookAt()
357{
358 if (m_lookAtNode)
359 lookAt(node: m_lookAtNode);
360}
361
362/*!
363 \qmlproperty float Camera::levelOfDetailBias
364 \since 6.5
365
366 This property changes the size a model needs to be when rendered before the
367 automatic level of detail meshes are used. Each generated level of detail
368 mesh contains an ideal size value that each level should be shown, which is
369 a ratio of how much of the rendered scene will be that mesh. A model that
370 represents only a few pixels on screen will not require the full geometry
371 to look correct, so a lower level of detail mesh will be used instead in
372 this case. This value is a bias to the ideal value such that a value smaller
373 than \c 1.0 will require an even smaller rendered size before switching to
374 a lesser level of detail. Values above \c 1.0 will lead to lower levels of detail
375 being used sooner. A value of \c 0.0 will disable the usage of levels of detail
376 completely.
377
378 The default value is \c 1.0
379
380 \note This property will only have an effect on Models with geomtry containing
381 levels of detail.
382
383 \sa Model::levelOfDetailBias
384*/
385
386float QQuick3DCamera::levelOfDetailBias() const
387{
388 return m_levelOfDetailBias;
389}
390
391void QQuick3DCamera::setLevelOfDetailBias(float newLevelOfDetailBias)
392{
393 if (qFuzzyCompare(p1: m_levelOfDetailBias, p2: newLevelOfDetailBias))
394 return;
395 m_levelOfDetailBias = newLevelOfDetailBias;
396 emit levelOfDetailBiasChanged();
397 update();
398}
399
400QT_END_NAMESPACE
401
402

source code of qtquick3d/src/quick3d/qquick3dcamera.cpp