| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2015 Klaralvdalens Datakonsult AB (KDAB). |
| 4 | ** Contact: https://www.qt.io/licensing/ |
| 5 | ** |
| 6 | ** This file is part of the Qt3D module of the Qt Toolkit. |
| 7 | ** |
| 8 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 9 | ** Commercial License Usage |
| 10 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 11 | ** accordance with the commercial license agreement provided with the |
| 12 | ** Software or, alternatively, in accordance with the terms contained in |
| 13 | ** a written agreement between you and The Qt Company. For licensing terms |
| 14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 15 | ** information use the contact form at https://www.qt.io/contact-us. |
| 16 | ** |
| 17 | ** GNU General Public License Usage |
| 18 | ** Alternatively, this file may be used under the terms of the GNU |
| 19 | ** General Public License version 3 as published by the Free Software |
| 20 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 21 | ** included in the packaging of this file. Please review the following |
| 22 | ** information to ensure the GNU General Public License requirements will |
| 23 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 24 | ** |
| 25 | ** $QT_END_LICENSE$ |
| 26 | ** |
| 27 | ****************************************************************************/ |
| 28 | |
| 29 | #include <QtTest/QtTest> |
| 30 | #include <Qt3DRender/private/entity_p.h> |
| 31 | #include <Qt3DRender/private/qraycastingservice_p.h> |
| 32 | #include <Qt3DRender/private/sphere_p.h> |
| 33 | #include <Qt3DRender/private/entity_p.h> |
| 34 | #include <Qt3DRender/private/abstractpickingjob_p.h> |
| 35 | #include <Qt3DRender/private/qboundingvolumeprovider_p.h> |
| 36 | #include <Qt3DRender/private/qray3d_p.h> |
| 37 | #include <Qt3DRender/qcamera.h> |
| 38 | |
| 39 | using namespace Qt3DCore; |
| 40 | using namespace Qt3DRender; |
| 41 | using namespace Qt3DRender::Render; |
| 42 | using namespace Qt3DRender::RayCasting; |
| 43 | |
| 44 | class tst_RayCasting : public QObject |
| 45 | { |
| 46 | Q_OBJECT |
| 47 | public: |
| 48 | tst_RayCasting() {} |
| 49 | ~tst_RayCasting() {} |
| 50 | |
| 51 | private Q_SLOTS: |
| 52 | void shouldReturnValidHandle(); |
| 53 | void shouldReturnResultForEachHandle(); |
| 54 | void shouldReturnAllResults(); |
| 55 | void shouldReturnHits(); |
| 56 | void shouldReturnHits_data(); |
| 57 | void shouldIntersect_data(); |
| 58 | void shouldIntersect(); |
| 59 | void shouldUseProvidedBoudingVolumes(); |
| 60 | void mousePicking(); |
| 61 | |
| 62 | void cleanupTestCase(); |
| 63 | |
| 64 | private: |
| 65 | Sphere *volumeAt(int index); |
| 66 | QVector<Sphere> boundingVolumes; |
| 67 | }; |
| 68 | |
| 69 | void tst_RayCasting::shouldIntersect_data() |
| 70 | { |
| 71 | QTest::addColumn<QRay3D>(name: "ray" ); |
| 72 | QTest::addColumn<Sphere>(name: "sphere" ); |
| 73 | QTest::addColumn<bool>(name: "shouldIntersect" ); |
| 74 | |
| 75 | QRay3D ray(Vector3D(1, 1, 1), Vector3D(0, 0, 1)); |
| 76 | |
| 77 | Sphere sphere1(Vector3D(1, 1, 1), 2); |
| 78 | Sphere sphere2(Vector3D(0, 0, 0), 3); |
| 79 | Sphere sphere3(Vector3D(0, 1, 3), 1); |
| 80 | Sphere sphere4(Vector3D(4, 4, 5), 1); |
| 81 | Sphere sphere5(Vector3D(2, 2, 11), 5); |
| 82 | Sphere sphere6(Vector3D(2, 2, 13), 1); |
| 83 | Sphere sphere7(Vector3D(2, 2, 15), 5); |
| 84 | |
| 85 | QTest::newRow(dataTag: "Ray starts inside sphere" ) << ray << sphere1 << true; |
| 86 | QTest::newRow(dataTag: "Ray starts inside sphere" ) << ray << sphere2 << true; |
| 87 | QTest::newRow(dataTag: "Ray intersects sphere tangentially" ) << ray << sphere3 << true; |
| 88 | QTest::newRow(dataTag: "No intersection" ) << ray << sphere4 << false; |
| 89 | QTest::newRow(dataTag: "Ray intersect sphere" ) << ray << sphere5 << true; |
| 90 | QTest::newRow(dataTag: "No intersection" ) << ray << sphere6 << false; |
| 91 | QTest::newRow(dataTag: "Ray intersect sphere" ) << ray << sphere7 << true; |
| 92 | } |
| 93 | |
| 94 | void tst_RayCasting::shouldIntersect() |
| 95 | { |
| 96 | QFETCH(QRay3D, ray); |
| 97 | QFETCH(Sphere, sphere); |
| 98 | QFETCH(bool, shouldIntersect); |
| 99 | |
| 100 | Vector3D intersectionPoint; |
| 101 | |
| 102 | QCOMPARE(sphere.intersects(ray, &intersectionPoint), shouldIntersect); |
| 103 | } |
| 104 | |
| 105 | class MyBoudingVolumesProvider : public QBoundingVolumeProvider |
| 106 | { |
| 107 | public: |
| 108 | MyBoudingVolumesProvider(QVector<QBoundingVolume *> volumes) |
| 109 | : m_volumes(volumes) |
| 110 | {} |
| 111 | |
| 112 | QVector<QBoundingVolume *> boundingVolumes() const |
| 113 | { |
| 114 | return m_volumes; |
| 115 | } |
| 116 | |
| 117 | private: |
| 118 | QVector<QBoundingVolume *> m_volumes; |
| 119 | }; |
| 120 | |
| 121 | void tst_RayCasting::shouldReturnValidHandle() |
| 122 | { |
| 123 | // GIVEN |
| 124 | QRay3D ray; |
| 125 | Sphere v1; |
| 126 | MyBoudingVolumesProvider provider = QVector<QBoundingVolume *>() << &v1; |
| 127 | |
| 128 | QRayCastingService service; |
| 129 | |
| 130 | // WHEN |
| 131 | QQueryHandle handle = service.query(ray, |
| 132 | mode: QAbstractCollisionQueryService::AllHits, |
| 133 | provider: &provider); |
| 134 | |
| 135 | // THEN |
| 136 | QVERIFY(handle >= 0); |
| 137 | |
| 138 | // Wait the query to finish |
| 139 | service.fetchResult(handle); |
| 140 | } |
| 141 | |
| 142 | void tst_RayCasting::shouldReturnResultForEachHandle() |
| 143 | { |
| 144 | // GIVEN |
| 145 | QRay3D ray; |
| 146 | QVector<QBoundingVolume *> volumes; |
| 147 | MyBoudingVolumesProvider provider(volumes); |
| 148 | |
| 149 | QRayCastingService service; |
| 150 | |
| 151 | QQueryHandle handle1 = service.query(ray, |
| 152 | mode: QAbstractCollisionQueryService::AllHits, |
| 153 | provider: &provider); |
| 154 | QQueryHandle handle2 = service.query(ray, |
| 155 | mode: QAbstractCollisionQueryService::FirstHit, |
| 156 | provider: &provider); |
| 157 | |
| 158 | // WHEN |
| 159 | QCollisionQueryResult result2 = service.fetchResult(handle: handle2); |
| 160 | QCollisionQueryResult result1 = service.fetchResult(handle: handle1); |
| 161 | |
| 162 | // THEN |
| 163 | QCOMPARE(result1.handle(), handle1); |
| 164 | QCOMPARE(result2.handle(), handle2); |
| 165 | } |
| 166 | |
| 167 | void tst_RayCasting::shouldReturnAllResults() |
| 168 | { |
| 169 | // GIVEN |
| 170 | QRay3D ray; |
| 171 | QVector<QBoundingVolume *> volumes; |
| 172 | MyBoudingVolumesProvider provider(volumes); |
| 173 | |
| 174 | QRayCastingService service; |
| 175 | |
| 176 | QVector<QQueryHandle> handles; |
| 177 | handles.append(t: service.query(ray, |
| 178 | mode: QAbstractCollisionQueryService::AllHits, |
| 179 | provider: &provider)); |
| 180 | handles.append(t: service.query(ray, |
| 181 | mode: QAbstractCollisionQueryService::FirstHit, |
| 182 | provider: &provider)); |
| 183 | |
| 184 | // WHEN |
| 185 | const QVector<QCollisionQueryResult> results = service.fetchAllResults(); |
| 186 | |
| 187 | // THEN |
| 188 | bool expectedHandlesFound = true; |
| 189 | for (QQueryHandle expected : qAsConst(t&: handles)) { |
| 190 | bool found = false; |
| 191 | for (QCollisionQueryResult result : results) { |
| 192 | if (result.handle() == expected) |
| 193 | found = true; |
| 194 | } |
| 195 | |
| 196 | expectedHandlesFound &= found; |
| 197 | } |
| 198 | |
| 199 | QVERIFY(expectedHandlesFound); |
| 200 | } |
| 201 | |
| 202 | void tst_RayCasting::shouldReturnHits_data() |
| 203 | { |
| 204 | QTest::addColumn<QRay3D>(name: "ray" ); |
| 205 | QTest::addColumn<QVector<QBoundingVolume *> >(name: "volumes" ); |
| 206 | QTest::addColumn<QVector<QNodeId> >(name: "hits" ); |
| 207 | QTest::addColumn<QAbstractCollisionQueryService::QueryMode >(name: "queryMode" ); |
| 208 | |
| 209 | QRay3D ray(Vector3D(1, 1, 1), Vector3D(0, 0, 1)); |
| 210 | |
| 211 | this->boundingVolumes.clear(); |
| 212 | this->boundingVolumes.append(l: QVector<Sphere>() << Sphere(Vector3D(1, 1, 1), 3, QNodeId::createId()) |
| 213 | << Sphere(Vector3D(0, 0, 0), 3, QNodeId::createId()) |
| 214 | << Sphere(Vector3D(0, 1, 3), 1, QNodeId::createId()) |
| 215 | << Sphere(Vector3D(4, 4, 5), 1, QNodeId::createId()) |
| 216 | << Sphere(Vector3D(2, 2, 11), 5, QNodeId::createId()) |
| 217 | << Sphere(Vector3D(2, 2, 13), 1, QNodeId::createId()) |
| 218 | << Sphere(Vector3D(2, 2, 15), 5, QNodeId::createId())); |
| 219 | |
| 220 | QTest::newRow(dataTag: "All hits, One sphere intersect" ) << ray |
| 221 | << (QVector<QBoundingVolume *> () << volumeAt(index: 0) << volumeAt(index: 3)) |
| 222 | << (QVector<QNodeId>() << volumeAt(index: 0)->id()) |
| 223 | << QAbstractCollisionQueryService::AllHits; |
| 224 | |
| 225 | QTest::newRow(dataTag: "All hits, Three sphere intersect" ) << ray |
| 226 | << (QVector<QBoundingVolume *> () << volumeAt(index: 0) << volumeAt(index: 3) << volumeAt(index: 6) << volumeAt(index: 2)) |
| 227 | << (QVector<QNodeId>() << volumeAt(index: 0)->id() << volumeAt(index: 2)->id() << volumeAt(index: 6)->id()) |
| 228 | << QAbstractCollisionQueryService::AllHits; |
| 229 | |
| 230 | QTest::newRow(dataTag: "All hits, No sphere intersect" ) << ray |
| 231 | << (QVector<QBoundingVolume *> () << volumeAt(index: 3) << volumeAt(index: 5)) |
| 232 | << (QVector<QNodeId>()) |
| 233 | << QAbstractCollisionQueryService::AllHits; |
| 234 | |
| 235 | QTest::newRow(dataTag: "Sphere 1 intersect, returns First Hit" ) << ray |
| 236 | << (QVector<QBoundingVolume *> () << volumeAt(index: 0) << volumeAt(index: 3) << volumeAt(index: 6)) |
| 237 | << (QVector<QNodeId>() << volumeAt(index: 0)->id()) |
| 238 | << QAbstractCollisionQueryService::FirstHit; |
| 239 | |
| 240 | QTest::newRow(dataTag: "Sphere 3 and 5 intersects, returns First Hit" ) << ray |
| 241 | << (QVector<QBoundingVolume *> () << volumeAt(index: 3) << volumeAt(index: 6) << volumeAt(index: 4)) |
| 242 | << (QVector<QNodeId>() << volumeAt(index: 4)->id()) |
| 243 | << QAbstractCollisionQueryService::FirstHit; |
| 244 | |
| 245 | QTest::newRow(dataTag: "Sphere 5 and 3 intersects, unordered list, returns First Hit" ) << ray |
| 246 | << (QVector<QBoundingVolume *> () << volumeAt(index: 4) << volumeAt(index: 3) << volumeAt(index: 6)) |
| 247 | << (QVector<QNodeId>() << volumeAt(index: 4)->id()) |
| 248 | << QAbstractCollisionQueryService::FirstHit; |
| 249 | |
| 250 | QTest::newRow(dataTag: "No sphere intersect, returns First Hit" ) << ray |
| 251 | << (QVector<QBoundingVolume *> () << volumeAt(index: 3) << volumeAt(index: 5)) |
| 252 | << (QVector<QNodeId>()) |
| 253 | << QAbstractCollisionQueryService::FirstHit; |
| 254 | } |
| 255 | |
| 256 | void tst_RayCasting::shouldReturnHits() |
| 257 | { |
| 258 | // GIVEN |
| 259 | QFETCH(QRay3D, ray); |
| 260 | QFETCH(QVector<QBoundingVolume *>, volumes); |
| 261 | QFETCH(QVector<QNodeId>, hits); |
| 262 | QFETCH(QAbstractCollisionQueryService::QueryMode, queryMode); |
| 263 | |
| 264 | MyBoudingVolumesProvider provider(volumes); |
| 265 | QRayCastingService service; |
| 266 | |
| 267 | // WHEN |
| 268 | QQueryHandle handle = service.query(ray, mode: queryMode, provider: &provider); |
| 269 | QCollisionQueryResult result = service.fetchResult(handle); |
| 270 | |
| 271 | // THEN |
| 272 | QCOMPARE(result.entitiesHit().size(), hits.size()); |
| 273 | QCOMPARE(result.entitiesHit(), hits); |
| 274 | } |
| 275 | |
| 276 | void tst_RayCasting::shouldUseProvidedBoudingVolumes() |
| 277 | { |
| 278 | // GIVEN |
| 279 | QRay3D ray(Vector3D(1, 1, 1), Vector3D(0, 0, 1)); |
| 280 | |
| 281 | Sphere sphere1(Vector3D(1, 1, 1), 3); |
| 282 | Sphere sphere3(Vector3D(0, 1, 3), 1); |
| 283 | Sphere sphere4(Vector3D(4, 4, 5), 1); |
| 284 | |
| 285 | MyBoudingVolumesProvider provider(QVector<QBoundingVolume *>() << &sphere1 << &sphere4 << &sphere3); |
| 286 | QVector<QNodeId> hits(QVector<QNodeId>() << sphere1.id() << sphere3.id()); |
| 287 | |
| 288 | QRayCastingService service; |
| 289 | |
| 290 | // WHEN |
| 291 | QQueryHandle handle = service.query(ray, |
| 292 | mode: QAbstractCollisionQueryService::AllHits, |
| 293 | provider: &provider); |
| 294 | QCollisionQueryResult result = service.fetchResult(handle); |
| 295 | |
| 296 | // THEN |
| 297 | QCOMPARE(result.entitiesHit().size(), hits.size()); |
| 298 | QCOMPARE(result.entitiesHit(), hits); |
| 299 | } |
| 300 | |
| 301 | void tst_RayCasting::cleanupTestCase() |
| 302 | { |
| 303 | this->boundingVolumes.clear(); |
| 304 | } |
| 305 | |
| 306 | Sphere *tst_RayCasting::volumeAt(int index) |
| 307 | { |
| 308 | return &*(boundingVolumes.begin() + index); |
| 309 | } |
| 310 | |
| 311 | void tst_RayCasting::mousePicking() |
| 312 | { |
| 313 | // GIVEN |
| 314 | Qt3DRender::QCamera camera; |
| 315 | camera.setProjectionType(QCameraLens::PerspectiveProjection); |
| 316 | camera.setFieldOfView(45.0f); |
| 317 | camera.setAspectRatio(800.0f/600.0f); |
| 318 | camera.setNearPlane(0.1f); |
| 319 | camera.setFarPlane(1000.0f); |
| 320 | camera.setPosition(QVector3D(0.0f, 0.0f, -40.0f)); |
| 321 | camera.setUpVector(QVector3D(0.0f, 1.0f, 0.0f)); |
| 322 | camera.setViewCenter(QVector3D(0.0f, 0.0f, 0.0f)); |
| 323 | |
| 324 | const QRectF viewport(0.0f, 0.0f, 800.0f, 600.0f); |
| 325 | |
| 326 | // Window center on near plane |
| 327 | QRay3D ray = Qt3DRender::Render::AbstractPickingJob::intersectionRay(pos: viewport.center().toPoint(), |
| 328 | viewMatrix: Matrix4x4(camera.viewMatrix()), |
| 329 | projectionMatrix: Matrix4x4(camera.projectionMatrix()), |
| 330 | viewport: viewport.toRect()); |
| 331 | Qt3DRender::Render::Sphere s(Vector3D(0.0f, 0.5f, 0.0f), 1.0f); |
| 332 | |
| 333 | // WHEN |
| 334 | bool intersects = s.intersects(ray, q: nullptr); |
| 335 | |
| 336 | // THEN |
| 337 | QVERIFY(intersects); |
| 338 | |
| 339 | // WHEN |
| 340 | ray = Qt3DRender::Render::AbstractPickingJob::intersectionRay(pos: viewport.topLeft().toPoint(), |
| 341 | viewMatrix: Matrix4x4(camera.viewMatrix()), |
| 342 | projectionMatrix: Matrix4x4(camera.projectionMatrix()), |
| 343 | viewport: viewport.toRect()); |
| 344 | intersects = s.intersects(ray, q: nullptr); |
| 345 | |
| 346 | // THEN |
| 347 | QVERIFY(!intersects); |
| 348 | |
| 349 | // WHEN |
| 350 | ray = Qt3DRender::Render::AbstractPickingJob::intersectionRay(pos: viewport.topRight().toPoint(), |
| 351 | viewMatrix: Matrix4x4(camera.viewMatrix()), |
| 352 | projectionMatrix: Matrix4x4(camera.projectionMatrix()), |
| 353 | viewport: viewport.toRect()); |
| 354 | intersects = s.intersects(ray, q: nullptr); |
| 355 | |
| 356 | // THEN |
| 357 | QVERIFY(!intersects); |
| 358 | |
| 359 | // WHEN |
| 360 | ray = Qt3DRender::Render::AbstractPickingJob::intersectionRay(pos: viewport.bottomLeft().toPoint(), |
| 361 | viewMatrix: Matrix4x4(camera.viewMatrix()), |
| 362 | projectionMatrix: Matrix4x4(camera.projectionMatrix()), |
| 363 | viewport: viewport.toRect()); |
| 364 | intersects = s.intersects(ray, q: nullptr); |
| 365 | |
| 366 | // THEN |
| 367 | QVERIFY(!intersects); |
| 368 | |
| 369 | // WHEN |
| 370 | ray = Qt3DRender::Render::AbstractPickingJob::intersectionRay(pos: viewport.bottomRight().toPoint(), |
| 371 | viewMatrix: Matrix4x4(camera.viewMatrix()), |
| 372 | projectionMatrix: Matrix4x4(camera.projectionMatrix()), |
| 373 | viewport: viewport.toRect()); |
| 374 | intersects = s.intersects(ray, q: nullptr); |
| 375 | |
| 376 | // THEN |
| 377 | QVERIFY(!intersects); |
| 378 | } |
| 379 | |
| 380 | QTEST_APPLESS_MAIN(tst_RayCasting) |
| 381 | |
| 382 | #include "tst_raycasting.moc" |
| 383 | |