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 | |
16 | QT_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 | */ |
72 | QQuick3DCamera::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 | */ |
84 | bool QQuick3DCamera::frustumCullingEnabled() const |
85 | { |
86 | return m_frustumCullingEnabled; |
87 | } |
88 | |
89 | void 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 | */ |
109 | QQuick3DNode *QQuick3DCamera::lookAtNode() const |
110 | { |
111 | return m_lookAtNode; |
112 | } |
113 | |
114 | void 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 | */ |
150 | QVector3D 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 | */ |
213 | QVector3D 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 | */ |
259 | QVector3D 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 | */ |
275 | QVector3D 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 | |
295 | void 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 | |
321 | void QQuick3DCamera::lookAt(QQuick3DNode *node) |
322 | { |
323 | if (!node) |
324 | return; |
325 | lookAt(scenePos: node->scenePosition()); |
326 | } |
327 | |
328 | void 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 | */ |
338 | QSSGRenderGraphObject *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 | |
356 | void 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 | |
386 | float QQuick3DCamera::levelOfDetailBias() const |
387 | { |
388 | return m_levelOfDetailBias; |
389 | } |
390 | |
391 | void 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 | |
400 | QT_END_NAMESPACE |
401 | |
402 | |