1// Copyright (C) 2020 Klaralvdalens Datakonsult AB (KDAB).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "calcboundingvolumejob_p.h"
5
6#include <Qt3DCore/qattribute.h>
7#include <Qt3DCore/qboundingvolume.h>
8#include <Qt3DCore/qbuffer.h>
9#include <Qt3DCore/qgeometryview.h>
10#include <Qt3DCore/qcoreaspect.h>
11#include <Qt3DCore/private/job_common_p.h>
12#include <Qt3DCore/private/qcoreaspect_p.h>
13#include <Qt3DCore/private/qaspectjob_p.h>
14#include <Qt3DCore/private/qaspectmanager_p.h>
15#include <Qt3DCore/private/qattribute_p.h>
16#include <Qt3DCore/private/qboundingvolume_p.h>
17#include <Qt3DCore/private/qbuffer_p.h>
18#include <Qt3DCore/private/qentity_p.h>
19#include <Qt3DCore/private/qgeometry_p.h>
20#include <Qt3DCore/private/qgeometryview_p.h>
21#include <Qt3DCore/private/qnodevisitor_p.h>
22#include <Qt3DCore/private/qthreadpooler_p.h>
23
24#include <QtCore/qmath.h>
25#if QT_CONFIG(concurrent)
26#include <QtConcurrent/QtConcurrent>
27#endif
28
29QT_BEGIN_NAMESPACE
30
31namespace Qt3DCore {
32
33namespace {
34
35BoundingVolumeComputeData findBoundingVolumeComputeData(QGeometryView *node)
36{
37 if (!node->isEnabled())
38 return {};
39
40 if (node->primitiveType() == QGeometryView::Patches)
41 return {};
42
43 QGeometry *geom = node->geometry();
44 QGeometryPrivate *dgeom = QGeometryPrivate::get(q: geom);
45 if (!geom)
46 return {};
47
48 int drawVertexCount = node->vertexCount(); // may be 0, gets changed below if so
49
50 QAttribute *positionAttribute = dgeom->m_boundingVolumePositionAttribute;
51 const QList<Qt3DCore::QAttribute *> attributes = geom->attributes();
52
53 // Use the default position attribute if attribute is null
54 if (!positionAttribute) {
55 for (QAttribute *attr : attributes) {
56 if (attr->name() == QAttribute::defaultPositionAttributeName()) {
57 positionAttribute = attr;
58 break;
59 }
60 }
61 }
62
63 if (!positionAttribute
64 || positionAttribute->attributeType() != QAttribute::VertexAttribute
65 || positionAttribute->vertexBaseType() != QAttribute::Float
66 || positionAttribute->vertexSize() < 3) {
67 qWarning(msg: "findBoundingVolumeComputeData: Position attribute not suited for bounding volume computation");
68 return {};
69 }
70
71 Qt3DCore::QBuffer *positionBuffer = positionAttribute->buffer();
72 // No point in continuing if the positionAttribute doesn't have a suitable buffer
73 if (!positionBuffer) {
74 qWarning(msg: "findBoundingVolumeComputeData: Position attribute not referencing a valid buffer");
75 return {};
76 }
77
78 // Check if there is an index attribute.
79 QAttribute *indexAttribute = nullptr;
80 Qt3DCore::QBuffer *indexBuffer = nullptr;
81
82 for (const auto attr : attributes) {
83 if (attr->attributeType() == QAttribute::IndexAttribute) {
84 indexBuffer = attr->buffer();
85 if (indexBuffer) {
86 indexAttribute = attr;
87
88 if (!drawVertexCount)
89 drawVertexCount = static_cast<int>(indexAttribute->count());
90
91 static const QAttribute::VertexBaseType validIndexTypes[] = {
92 QAttribute::UnsignedShort,
93 QAttribute::UnsignedInt,
94 QAttribute::UnsignedByte
95 };
96
97 if (std::find(first: std::begin(arr: validIndexTypes),
98 last: std::end(arr: validIndexTypes),
99 val: indexAttribute->vertexBaseType()) == std::end(arr: validIndexTypes)) {
100 qWarning() << "findBoundingVolumeComputeData: Unsupported index attribute type" << indexAttribute->name() << indexAttribute->vertexBaseType();
101 return {};
102 }
103
104 break;
105 }
106 }
107 }
108
109 if (!indexAttribute && !drawVertexCount)
110 drawVertexCount = static_cast<int>(positionAttribute->count());
111
112 return { .entity: nullptr, .provider: nullptr, .positionAttribute: positionAttribute, .indexAttribute: indexAttribute, .vertexCount: drawVertexCount };
113}
114
115bool isTreeEnabled(QEntity *entity) {
116 if (!entity->isEnabled())
117 return false;
118
119 QEntity *parent = entity->parentEntity();
120 while (parent) {
121 if (!parent->isEnabled())
122 return false;
123 parent = parent->parentEntity();
124 }
125
126 return true;
127}
128
129struct UpdateBoundFunctor
130{
131 // This define is required to work with QtConcurrent
132 typedef std::vector<BoundingVolumeComputeResult> result_type;
133 result_type operator ()(const BoundingVolumeComputeData &data)
134 {
135 return { data.compute() };
136 }
137};
138
139struct ReduceUpdateBoundFunctor
140{
141 void operator ()(std::vector<BoundingVolumeComputeResult> &result, const std::vector<BoundingVolumeComputeResult> &values)
142 {
143 result.insert(position: result.end(),
144 first: std::make_move_iterator(i: values.begin()),
145 last: std::make_move_iterator(i: values.end()));
146 }
147};
148
149} // anonymous
150
151
152BoundingVolumeComputeData BoundingVolumeComputeData::fromView(QGeometryView *view)
153{
154 return findBoundingVolumeComputeData(node: view);
155}
156
157BoundingVolumeComputeResult BoundingVolumeComputeData::compute() const
158{
159 BoundingVolumeCalculator calculator;
160 if (calculator.apply(positionAttribute, indexAttribute, drawVertexCount: vertexCount,
161 primitiveRestartEnabled: provider->view()->primitiveRestartEnabled(),
162 primitiveRestartIndex: provider->view()->restartIndexValue()))
163 return {
164 .entity: entity, .provider: provider, .positionAttribute: positionAttribute, .indexAttribute: indexAttribute,
165 .m_min: calculator.min(), .m_max: calculator.max(),
166 .m_center: calculator.center(), .m_radius: calculator.radius()
167 };
168 return {};
169}
170
171
172CalculateBoundingVolumeJob::CalculateBoundingVolumeJob(QCoreAspect *aspect)
173 : Qt3DCore::QAspectJob()
174 , m_aspect(aspect)
175 , m_root(nullptr)
176{
177 SET_JOB_RUN_STAT_TYPE(this, JobTypes::CalcBoundingVolume, 0)
178}
179
180bool CalculateBoundingVolumeJob::isRequired()
181{
182 if (!m_aspect)
183 return true;
184
185 auto daspect = QCoreAspectPrivate::get(aspect: m_aspect);
186 return daspect->m_boundingVolumesEnabled;
187}
188
189void CalculateBoundingVolumeJob::run()
190{
191 // There's 2 bounding volume jobs, one here in Core, the other in Render.
192 // - This one computes bounding volumes for entities that have QBoundingVolume
193 // components and use QGeometryViews.
194 // In that case the component is updated directly by this job (since core
195 // aspect does not maintain backend objects for the component)
196 // - The one in render does 2 things:
197 // . Copy the results of this job to the backend object for entities that
198 // use QBoundingVolume and QGeometryView (computed results arrive one
199 // frame later, explicit results arrive on time)
200 // . Compute the BV for old style QGeometryRenderer which use a QGeometry
201 // directly without a QGeometryView
202 //
203 // (see more details in Qt3DRender::CalculateBoundingVolumeJob::run)
204
205 m_results.clear();
206
207 QHash<QEntity *, BoundingVolumeComputeData> dirtyEntities;
208 QNodeVisitor visitor;
209 visitor.traverse(rootNode_: m_root, fN: [](QNode *) {}, fE: [&dirtyEntities, this](QEntity *entity) {
210 if (!isTreeEnabled(entity))
211 return;
212
213 const auto bvProviders = entity->componentsOfType<QBoundingVolume>();
214 if (bvProviders.isEmpty())
215 return;
216
217 // we go through the list until be find a dirty provider,
218 // or THE primary provider
219 bool foundBV = false;
220 for (auto bv: bvProviders) {
221 auto dbv = QBoundingVolumePrivate::get(q: bv);
222 if (foundBV && !dbv->m_primaryProvider)
223 continue;
224
225 BoundingVolumeComputeData bvdata;
226 if (dbv->m_explicitPointsValid) {
227 // we have data explicitly set by the user, pass it to the
228 // watchers as computed data
229 BoundingVolumeComputeResult r;
230 r.entity = entity;
231 r.provider = bv;
232 r.m_min = dbv->m_minPoint;
233 r.m_max = dbv->m_maxPoint;
234 const auto diagonal = r.m_max - r.m_min;
235 r.m_center = r.m_min + diagonal * .5f;
236 r.m_radius = diagonal.length();
237
238 for (auto w: m_watchers) {
239 auto wp = w.toStrongRef();
240 if (wp)
241 wp->process(result: r, computedResult: false);
242 }
243 continue;
244 } else if (bv->view()) {
245 bvdata = findBoundingVolumeComputeData(node: bv->view());
246 if (!bvdata.valid())
247 continue;
248 bvdata.entity = entity;
249 bvdata.provider = bv;
250 } else {
251 // no view, can't compute
252 continue;
253 }
254
255 bool dirty = QEntityPrivate::get(q: entity)->m_dirty;
256 dirty |= QGeometryViewPrivate::get(q: bv->view())->m_dirty;
257 dirty |= QGeometryPrivate::get(q: bv->view()->geometry())->m_dirty;
258 dirty |= QAttributePrivate::get(q: bvdata.positionAttribute)->m_dirty;
259 dirty |= QBufferPrivate::get(q: bvdata.positionAttribute->buffer())->m_dirty;
260 if (bvdata.indexAttribute) {
261 dirty |= QAttributePrivate::get(q: bvdata.indexAttribute)->m_dirty;
262 dirty |= QBufferPrivate::get(q: bvdata.indexAttribute->buffer())->m_dirty;
263 }
264
265 if (dbv->m_primaryProvider) {
266 if (dirty)
267 dirtyEntities[entity] = bvdata;
268 break;
269 } else if (dirty) {
270 dirtyEntities[entity] = bvdata;
271 foundBV = true;
272 }
273 }
274 });
275
276#if QT_CONFIG(concurrent)
277 if (dirtyEntities.size() > 1 && QAspectJobManager::idealThreadCount() > 1) {
278 UpdateBoundFunctor functor;
279 ReduceUpdateBoundFunctor reduceFunctor;
280 m_results = QtConcurrent::blockingMappedReduced<decltype(m_results)>(sequence&: dirtyEntities, map&: functor, reduce&: reduceFunctor);
281 } else
282#endif
283 {
284 for (auto it = dirtyEntities.begin(); it != dirtyEntities.end(); ++it) {
285 auto res = it.value().compute();
286 if (res.valid())
287 m_results.push_back(x: res); // How do we push it to the backends????
288 }
289 }
290
291 // pass the computed results to the watchers
292 for (auto &watcher: m_watchers) {
293 auto watcherPtr = watcher.toStrongRef();
294 if (watcherPtr) {
295 for (const auto &r: m_results)
296 watcherPtr->process(result: r, computedResult: true);
297 }
298 }
299}
300
301void CalculateBoundingVolumeJob::postFrame(QAspectEngine *aspectEngine)
302{
303 Q_UNUSED(aspectEngine);
304
305 for (auto result: m_results) {
306 // set the results
307 QBoundingVolumePrivate::get(q: result.provider)->setImplicitBounds(minPoint: result.m_min, maxPoint: result.m_max, center: result.m_center, radius: result.m_radius);
308
309 // reset dirty flags
310 QEntityPrivate::get(q: result.entity)->m_dirty = false;
311 QGeometryViewPrivate::get(q: result.provider->view())->m_dirty = false;
312 QGeometryPrivate::get(q: result.provider->view()->geometry())->m_dirty = false;
313 QAttributePrivate::get(q: result.positionAttribute)->m_dirty = false;
314 QBufferPrivate::get(q: result.positionAttribute->buffer())->m_dirty = false;
315 if (result.indexAttribute) {
316 QAttributePrivate::get(q: result.indexAttribute)->m_dirty = false;
317 QBufferPrivate::get(q: result.indexAttribute->buffer())->m_dirty = false;
318 }
319 }
320
321 m_results.clear();
322}
323
324void CalculateBoundingVolumeJob::addWatcher(QWeakPointer<BoundingVolumeJobProcessor> watcher)
325{
326 m_watchers.push_back(x: watcher);
327}
328
329void CalculateBoundingVolumeJob::removeWatcher(QWeakPointer<BoundingVolumeJobProcessor> watcher)
330{
331 if (watcher.isNull()) {
332 m_watchers.erase(first: std::remove_if(first: m_watchers.begin(), last: m_watchers.end(), pred: [](const QWeakPointer<BoundingVolumeJobProcessor> &w) { return w.isNull(); }),
333 last: m_watchers.end());
334 } else {
335 m_watchers.erase(first: std::remove(first: m_watchers.begin(), last: m_watchers.end(), value: watcher),
336 last: m_watchers.end());
337 }
338}
339
340} // namespace Qt3DCore
341
342QT_END_NAMESPACE
343
344

source code of qt3d/src/core/jobs/calcboundingvolumejob.cpp