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 for (const auto &value : values) {
144 if (value.valid()) {
145 result.push_back(x: value);
146 }
147 }
148 }
149};
150
151} // anonymous
152
153
154BoundingVolumeComputeData BoundingVolumeComputeData::fromView(QGeometryView *view)
155{
156 return findBoundingVolumeComputeData(node: view);
157}
158
159BoundingVolumeComputeResult BoundingVolumeComputeData::compute() const
160{
161 BoundingVolumeCalculator calculator;
162 if (calculator.apply(positionAttribute, indexAttribute, drawVertexCount: vertexCount,
163 primitiveRestartEnabled: provider->view()->primitiveRestartEnabled(),
164 primitiveRestartIndex: provider->view()->restartIndexValue()))
165 return {
166 .entity: entity, .provider: provider, .positionAttribute: positionAttribute, .indexAttribute: indexAttribute,
167 .m_min: calculator.min(), .m_max: calculator.max(),
168 .m_center: calculator.center(), .m_radius: calculator.radius()
169 };
170 return {};
171}
172
173
174CalculateBoundingVolumeJob::CalculateBoundingVolumeJob(QCoreAspect *aspect)
175 : Qt3DCore::QAspectJob()
176 , m_aspect(aspect)
177 , m_root(nullptr)
178{
179 SET_JOB_RUN_STAT_TYPE(this, JobTypes::CalcBoundingVolume, 0)
180}
181
182bool CalculateBoundingVolumeJob::isRequired()
183{
184 if (!m_aspect)
185 return true;
186
187 auto daspect = QCoreAspectPrivate::get(aspect: m_aspect);
188 return daspect->m_boundingVolumesEnabled;
189}
190
191void CalculateBoundingVolumeJob::run()
192{
193 // There's 2 bounding volume jobs, one here in Core, the other in Render.
194 // - This one computes bounding volumes for entities that have QBoundingVolume
195 // components and use QGeometryViews.
196 // In that case the component is updated directly by this job (since core
197 // aspect does not maintain backend objects for the component)
198 // - The one in render does 2 things:
199 // . Copy the results of this job to the backend object for entities that
200 // use QBoundingVolume and QGeometryView (computed results arrive one
201 // frame later, explicit results arrive on time)
202 // . Compute the BV for old style QGeometryRenderer which use a QGeometry
203 // directly without a QGeometryView
204 //
205 // (see more details in Qt3DRender::CalculateBoundingVolumeJob::run)
206
207 m_results.clear();
208
209 QHash<QEntity *, BoundingVolumeComputeData> dirtyEntities;
210 QNodeVisitor visitor;
211 visitor.traverse(rootNode_: m_root, fN: [](QNode *) {}, fE: [&dirtyEntities, this](QEntity *entity) {
212 if (!isTreeEnabled(entity))
213 return;
214
215 const auto bvProviders = entity->componentsOfType<QBoundingVolume>();
216 if (bvProviders.isEmpty())
217 return;
218
219 // we go through the list until be find a dirty provider,
220 // or THE primary provider
221 bool foundBV = false;
222 for (auto bv: bvProviders) {
223 auto dbv = QBoundingVolumePrivate::get(q: bv);
224 if (foundBV && !dbv->m_primaryProvider)
225 continue;
226
227 BoundingVolumeComputeData bvdata;
228 if (dbv->m_explicitPointsValid) {
229 // we have data explicitly set by the user, pass it to the
230 // watchers as computed data
231 BoundingVolumeComputeResult r;
232 r.entity = entity;
233 r.provider = bv;
234 r.m_min = dbv->m_minPoint;
235 r.m_max = dbv->m_maxPoint;
236 const auto diagonal = r.m_max - r.m_min;
237 r.m_center = r.m_min + diagonal * .5f;
238 r.m_radius = diagonal.length();
239
240 for (auto w: m_watchers) {
241 auto wp = w.toStrongRef();
242 if (wp)
243 wp->process(result: r, computedResult: false);
244 }
245 continue;
246 } else if (bv->view()) {
247 bvdata = findBoundingVolumeComputeData(node: bv->view());
248 if (!bvdata.valid())
249 continue;
250 bvdata.entity = entity;
251 bvdata.provider = bv;
252 } else {
253 // no view, can't compute
254 continue;
255 }
256
257 bool dirty = QEntityPrivate::get(q: entity)->m_dirty;
258 dirty |= QGeometryViewPrivate::get(q: bv->view())->m_dirty;
259 dirty |= QGeometryPrivate::get(q: bv->view()->geometry())->m_dirty;
260 dirty |= QAttributePrivate::get(q: bvdata.positionAttribute)->m_dirty;
261 dirty |= QBufferPrivate::get(q: bvdata.positionAttribute->buffer())->m_dirty;
262 if (bvdata.indexAttribute) {
263 dirty |= QAttributePrivate::get(q: bvdata.indexAttribute)->m_dirty;
264 dirty |= QBufferPrivate::get(q: bvdata.indexAttribute->buffer())->m_dirty;
265 }
266
267 if (dbv->m_primaryProvider) {
268 if (dirty)
269 dirtyEntities[entity] = bvdata;
270 break;
271 } else if (dirty) {
272 dirtyEntities[entity] = bvdata;
273 foundBV = true;
274 }
275 }
276 });
277
278#if QT_CONFIG(concurrent)
279 if (dirtyEntities.size() > 1 && QAspectJobManager::idealThreadCount() > 1) {
280 UpdateBoundFunctor functor;
281 ReduceUpdateBoundFunctor reduceFunctor;
282 m_results = QtConcurrent::blockingMappedReduced<decltype(m_results)>(sequence&: dirtyEntities, map&: functor, reduce&: reduceFunctor);
283 } else
284#endif
285 {
286 for (auto it = dirtyEntities.begin(); it != dirtyEntities.end(); ++it) {
287 auto res = it.value().compute();
288 if (res.valid())
289 m_results.push_back(x: res); // How do we push it to the backends????
290 }
291 }
292
293 // pass the computed results to the watchers
294 for (auto &watcher: m_watchers) {
295 auto watcherPtr = watcher.toStrongRef();
296 if (watcherPtr) {
297 for (const auto &r: m_results)
298 watcherPtr->process(result: r, computedResult: true);
299 }
300 }
301}
302
303void CalculateBoundingVolumeJob::postFrame(QAspectEngine *aspectEngine)
304{
305 Q_UNUSED(aspectEngine);
306
307 for (auto result: m_results) {
308 // set the results
309 QBoundingVolumePrivate::get(q: result.provider)->setImplicitBounds(minPoint: result.m_min, maxPoint: result.m_max, center: result.m_center, radius: result.m_radius);
310
311 // reset dirty flags
312 QEntityPrivate::get(q: result.entity)->m_dirty = false;
313 QGeometryViewPrivate::get(q: result.provider->view())->m_dirty = false;
314 QGeometryPrivate::get(q: result.provider->view()->geometry())->m_dirty = false;
315 QAttributePrivate::get(q: result.positionAttribute)->m_dirty = false;
316 QBufferPrivate::get(q: result.positionAttribute->buffer())->m_dirty = false;
317 if (result.indexAttribute) {
318 QAttributePrivate::get(q: result.indexAttribute)->m_dirty = false;
319 QBufferPrivate::get(q: result.indexAttribute->buffer())->m_dirty = false;
320 }
321 }
322
323 m_results.clear();
324}
325
326void CalculateBoundingVolumeJob::addWatcher(QWeakPointer<BoundingVolumeJobProcessor> watcher)
327{
328 m_watchers.push_back(x: watcher);
329}
330
331void CalculateBoundingVolumeJob::removeWatcher(QWeakPointer<BoundingVolumeJobProcessor> watcher)
332{
333 if (watcher.isNull()) {
334 m_watchers.erase(first: std::remove_if(first: m_watchers.begin(), last: m_watchers.end(), pred: [](const QWeakPointer<BoundingVolumeJobProcessor> &w) { return w.isNull(); }),
335 last: m_watchers.end());
336 } else {
337 m_watchers.erase(first: std::remove(first: m_watchers.begin(), last: m_watchers.end(), value: watcher),
338 last: m_watchers.end());
339 }
340}
341
342} // namespace Qt3DCore
343
344QT_END_NAMESPACE
345
346

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