| 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 real 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 | |