| 1 | // Copyright (C) 2008-2012 NVIDIA Corporation. |
| 2 | // Copyright (C) 2019 The Qt Company Ltd. |
| 3 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 4 | |
| 5 | #ifndef QSSG_RENDER_IMPL_RENDERABLE_OBJECTS_H |
| 6 | #define QSSG_RENDER_IMPL_RENDERABLE_OBJECTS_H |
| 7 | |
| 8 | // |
| 9 | // W A R N I N G |
| 10 | // ------------- |
| 11 | // |
| 12 | // This file is not part of the Qt API. It exists purely as an |
| 13 | // implementation detail. This header file may change from version to |
| 14 | // version without notice, or even be removed. |
| 15 | // |
| 16 | // We mean it. |
| 17 | // |
| 18 | |
| 19 | #include <QtQuick3DRuntimeRender/private/qssgrendermodel_p.h> |
| 20 | #include <QtQuick3DRuntimeRender/private/qssgrenderdefaultmaterial_p.h> |
| 21 | #include <QtQuick3DRuntimeRender/private/qssgrendercustommaterial_p.h> |
| 22 | #include <QtQuick3DRuntimeRender/private/qssgrenderparticles_p.h> |
| 23 | #include <QtQuick3DRuntimeRender/private/qssgrendermesh_p.h> |
| 24 | #include <QtQuick3DRuntimeRender/private/qssgrendershaderkeys_p.h> |
| 25 | #include <QtQuick3DRuntimeRender/private/qssgrendershadercache_p.h> |
| 26 | #include <QtQuick3DRuntimeRender/private/qssgrenderableimage_p.h> |
| 27 | #include <QtQuick3DRuntimeRender/private/qssgrenderlight_p.h> |
| 28 | #include <QtQuick3DRuntimeRender/private/qssgrenderreflectionprobe_p.h> |
| 29 | #include <QtQuick3DRuntimeRender/private/qssgrenderclippingfrustum_p.h> |
| 30 | |
| 31 | #include <QtQuick3DUtils/private/qssginvasivelinkedlist_p.h> |
| 32 | |
| 33 | QT_BEGIN_NAMESPACE |
| 34 | |
| 35 | enum class QSSGRenderableObjectFlag : quint32 |
| 36 | { |
| 37 | HasTransparency = 1 << 0, |
| 38 | CompletelyTransparent = 1 << 1, |
| 39 | Dirty = 1 << 2, |
| 40 | CastsShadows = 1 << 3, |
| 41 | ReceivesShadows = 1 << 4, |
| 42 | HasAttributePosition = 1 << 5, |
| 43 | HasAttributeNormal = 1 << 6, |
| 44 | HasAttributeTexCoord0 = 1 << 7, |
| 45 | HasAttributeTexCoord1 = 1 << 8, |
| 46 | HasAttributeTangent = 1 << 9, |
| 47 | HasAttributeBinormal = 1 << 10, |
| 48 | HasAttributeColor = 1 << 11, |
| 49 | HasAttributeJointAndWeight = 1 << 12, |
| 50 | IsPointsTopology = 1 << 13, |
| 51 | // The number of target models' attributes are too many |
| 52 | // to store in a renderable flag. |
| 53 | // They will be recorded in shaderKey. |
| 54 | HasAttributeMorphTarget = 1 << 14, |
| 55 | RequiresScreenTexture = 1 << 15, |
| 56 | ReceivesReflections = 1 << 16, |
| 57 | UsedInBakedLighting = 1 << 17, |
| 58 | RendersWithLightmap = 1 << 18, |
| 59 | HasAttributeTexCoordLightmap = 1 << 19, |
| 60 | CastsReflections = 1 << 20 |
| 61 | }; |
| 62 | |
| 63 | struct QSSGRenderableObjectFlags : public QFlags<QSSGRenderableObjectFlag> |
| 64 | { |
| 65 | void setHasTransparency(bool inHasTransparency) |
| 66 | { |
| 67 | setFlag(flag: QSSGRenderableObjectFlag::HasTransparency, on: inHasTransparency); |
| 68 | } |
| 69 | bool hasTransparency() const { return this->operator&(other: QSSGRenderableObjectFlag::HasTransparency); } |
| 70 | void setCompletelyTransparent(bool inTransparent) |
| 71 | { |
| 72 | setFlag(flag: QSSGRenderableObjectFlag::CompletelyTransparent, on: inTransparent); |
| 73 | } |
| 74 | bool isCompletelyTransparent() const |
| 75 | { |
| 76 | return this->operator&(other: QSSGRenderableObjectFlag::CompletelyTransparent); |
| 77 | } |
| 78 | void setDirty(bool inDirty) { setFlag(flag: QSSGRenderableObjectFlag::Dirty, on: inDirty); } |
| 79 | bool isDirty() const { return this->operator&(other: QSSGRenderableObjectFlag::Dirty); } |
| 80 | |
| 81 | void setCastsShadows(bool inCastsShadows) { setFlag(flag: QSSGRenderableObjectFlag::CastsShadows, on: inCastsShadows); } |
| 82 | bool castsShadows() const { return this->operator&(other: QSSGRenderableObjectFlag::CastsShadows); } |
| 83 | |
| 84 | void setReceivesShadows(bool inReceivesShadows) { setFlag(flag: QSSGRenderableObjectFlag::ReceivesShadows, on: inReceivesShadows); } |
| 85 | bool receivesShadows() const { return this->operator&(other: QSSGRenderableObjectFlag::ReceivesShadows); } |
| 86 | |
| 87 | void setReceivesReflections(bool inReceivesReflections) { setFlag(flag: QSSGRenderableObjectFlag::ReceivesReflections, on: inReceivesReflections); } |
| 88 | bool receivesReflections() const { return this->operator&(other: QSSGRenderableObjectFlag::ReceivesReflections); } |
| 89 | |
| 90 | void setCastsReflections(bool inCastsReflections) { setFlag(flag: QSSGRenderableObjectFlag::CastsReflections, on: inCastsReflections); } |
| 91 | bool castsReflections() const { return this->operator&(other: QSSGRenderableObjectFlag::CastsReflections); } |
| 92 | |
| 93 | void setUsedInBakedLighting(bool inUsedInBakedLighting) { setFlag(flag: QSSGRenderableObjectFlag::UsedInBakedLighting, on: inUsedInBakedLighting); } |
| 94 | bool usedInBakedLighting() const { return this->operator&(other: QSSGRenderableObjectFlag::UsedInBakedLighting); } |
| 95 | |
| 96 | void setRendersWithLightmap(bool inRendersWithLightmap) { setFlag(flag: QSSGRenderableObjectFlag::RendersWithLightmap, on: inRendersWithLightmap); } |
| 97 | bool rendersWithLightmap() const { return this->operator&(other: QSSGRenderableObjectFlag::RendersWithLightmap); } |
| 98 | |
| 99 | void setHasAttributePosition(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributePosition, on: b); } |
| 100 | bool hasAttributePosition() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributePosition); } |
| 101 | |
| 102 | void setHasAttributeNormal(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeNormal, on: b); } |
| 103 | bool hasAttributeNormal() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeNormal); } |
| 104 | |
| 105 | void setHasAttributeTexCoord0(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeTexCoord0, on: b); } |
| 106 | bool hasAttributeTexCoord0() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeTexCoord0); } |
| 107 | |
| 108 | void setHasAttributeTexCoord1(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeTexCoord1, on: b); } |
| 109 | bool hasAttributeTexCoord1() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeTexCoord1); } |
| 110 | |
| 111 | void setHasAttributeTexCoordLightmap(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeTexCoordLightmap, on: b); } |
| 112 | bool hasAttributeTexCoordLightmap() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeTexCoordLightmap); } |
| 113 | |
| 114 | void setHasAttributeTangent(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeTangent, on: b); } |
| 115 | bool hasAttributeTangent() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeTangent); } |
| 116 | |
| 117 | void setHasAttributeBinormal(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeBinormal, on: b); } |
| 118 | bool hasAttributeBinormal() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeBinormal); } |
| 119 | |
| 120 | void setHasAttributeColor(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeColor, on: b); } |
| 121 | bool hasAttributeColor() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeColor); } |
| 122 | |
| 123 | void setHasAttributeJointAndWeight(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeJointAndWeight, on: b); |
| 124 | } |
| 125 | bool hasAttributeJointAndWeight() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeJointAndWeight); } |
| 126 | |
| 127 | void setHasAttributeMorphTarget(bool b) { setFlag(flag: QSSGRenderableObjectFlag::HasAttributeMorphTarget, on: b); |
| 128 | } |
| 129 | bool hasAttributeMorphTarget() const { return this->operator&(other: QSSGRenderableObjectFlag::HasAttributeMorphTarget); } |
| 130 | |
| 131 | void setPointsTopology(bool v) |
| 132 | { |
| 133 | setFlag(flag: QSSGRenderableObjectFlag::IsPointsTopology, on: v); |
| 134 | } |
| 135 | bool isPointsTopology() const |
| 136 | { |
| 137 | return this->operator&(other: QSSGRenderableObjectFlag::IsPointsTopology); |
| 138 | } |
| 139 | void setRequiresScreenTexture(bool v) |
| 140 | { |
| 141 | setFlag(flag: QSSGRenderableObjectFlag::RequiresScreenTexture, on: v); |
| 142 | } |
| 143 | bool requiresScreenTexture() const { |
| 144 | return this->operator&(other: QSSGRenderableObjectFlag::RequiresScreenTexture); |
| 145 | } |
| 146 | }; |
| 147 | |
| 148 | struct QSSGShaderLight |
| 149 | { |
| 150 | QSSGRenderLight *light = nullptr; |
| 151 | bool shadows = false; |
| 152 | QVector3D direction; |
| 153 | |
| 154 | inline bool operator < (const QSSGShaderLight &o) const |
| 155 | { |
| 156 | // sort by light type |
| 157 | if (light->type < o.light->type) |
| 158 | return true; |
| 159 | // then shadow lights first |
| 160 | if (shadows > o.shadows) |
| 161 | return true; |
| 162 | return false; |
| 163 | } |
| 164 | }; |
| 165 | |
| 166 | struct QSSGShaderReflectionProbe |
| 167 | { |
| 168 | QVector3D probeCubeMapCenter; |
| 169 | QVector3D probeBoxMax; |
| 170 | QVector3D probeBoxMin; |
| 171 | bool enabled = false; |
| 172 | int parallaxCorrection = 0; |
| 173 | }; |
| 174 | |
| 175 | // Having this as a QVLA is beneficial mainly because QVector would need to |
| 176 | // detach somewhere in QSSGLayerRenderPreparationData::prepareForRender so the |
| 177 | // implicit sharing's benefits do not outweigh the cost of allocations in this case. |
| 178 | typedef QVarLengthArray<QSSGShaderLight, 16> QSSGShaderLightList; |
| 179 | using QSSGShaderLightListView = QSSGDataView<QSSGShaderLight>; |
| 180 | |
| 181 | struct QSSGRenderableObject; |
| 182 | |
| 183 | struct QSSGRenderableNodeEntry |
| 184 | { |
| 185 | enum Overridden : quint16 |
| 186 | { |
| 187 | Original = 0, |
| 188 | Disabled = 0x1, |
| 189 | GlobalTransform = 0x2, |
| 190 | Materials = 0x4, |
| 191 | GlobalOpacity = 0x5 |
| 192 | }; |
| 193 | |
| 194 | QSSGRenderNode *node = nullptr; |
| 195 | // TODO: We should have an index here for look-up and store the data in a table, |
| 196 | // er already have the index from when we collect the nodes. We might cull some items at a later |
| 197 | // stage but that should be fine. The sort data can be just a float and the index to this... |
| 198 | mutable QMatrix4x4 globalTransform; |
| 199 | mutable QSSGRenderMesh *mesh = nullptr; |
| 200 | mutable QVector<QSSGRenderGraphObject *> materials; |
| 201 | mutable QSSGShaderLightListView lights; |
| 202 | mutable float globalOpacity { 1.0f }; |
| 203 | mutable quint16 overridden { Original }; |
| 204 | |
| 205 | bool isNull() const { return (node == nullptr); } |
| 206 | QSSGRenderableNodeEntry() = default; |
| 207 | QSSGRenderableNodeEntry(QSSGRenderNode &inNode) : node(&inNode) {} |
| 208 | }; |
| 209 | |
| 210 | // Used for sorting |
| 211 | struct QSSGRenderableObjectHandle |
| 212 | { |
| 213 | QSSGRenderableObjectHandle() = default; |
| 214 | QSSGRenderableObjectHandle(QSSGRenderableObject *o, float camDistSq) |
| 215 | : obj(o) |
| 216 | , cameraDistanceSq(camDistSq) |
| 217 | {} |
| 218 | QSSGRenderableObject *obj = nullptr; |
| 219 | float cameraDistanceSq = 0.0f; |
| 220 | }; |
| 221 | Q_DECLARE_TYPEINFO(QSSGRenderableObjectHandle, Q_PRIMITIVE_TYPE); |
| 222 | |
| 223 | using QSSGRenderableObjectList = QVector<QSSGRenderableObjectHandle>; |
| 224 | |
| 225 | struct QSSGRenderableObject |
| 226 | { |
| 227 | enum class Type : quint8 |
| 228 | { |
| 229 | DefaultMaterialMeshSubset, |
| 230 | CustomMaterialMeshSubset, |
| 231 | Particles |
| 232 | }; |
| 233 | |
| 234 | // Variables used for picking |
| 235 | const QMatrix4x4 &globalTransform; |
| 236 | const QSSGBounds3 &bounds; |
| 237 | QSSGBounds3 globalBounds; |
| 238 | |
| 239 | QSSGRenderableObjectFlags renderableFlags; |
| 240 | // For rough sorting for transparency and for depth |
| 241 | QVector3D worldCenterPoint; |
| 242 | float depthBiasSq; // Squared as our sorting is based on the square distance! |
| 243 | float camdistSq = 0.0f; |
| 244 | QSSGDepthDrawMode depthWriteMode = QSSGDepthDrawMode::OpaqueOnly; |
| 245 | const Type type; |
| 246 | float instancingLodMin = -1; |
| 247 | float instancingLodMax = -1; |
| 248 | |
| 249 | QSSGRenderableObject(Type ty, |
| 250 | QSSGRenderableObjectFlags inFlags, |
| 251 | const QVector3D &inWorldCenterPt, |
| 252 | const QMatrix4x4 &inGlobalTransform, |
| 253 | const QSSGBounds3 &inBounds, |
| 254 | float inDepthBias, |
| 255 | float inMinThreshold = -1, |
| 256 | float inMaxThreshold = -1) |
| 257 | |
| 258 | : globalTransform(inGlobalTransform) |
| 259 | , bounds(inBounds) |
| 260 | , globalBounds(inBounds) |
| 261 | , renderableFlags(inFlags) |
| 262 | , worldCenterPoint(inWorldCenterPt) |
| 263 | , depthBiasSq(inDepthBias) |
| 264 | , type(ty) |
| 265 | , instancingLodMin(inMinThreshold) |
| 266 | , instancingLodMax(inMaxThreshold) |
| 267 | { |
| 268 | } |
| 269 | }; |
| 270 | |
| 271 | Q_STATIC_ASSERT(std::is_trivially_destructible<QSSGRenderableObject>::value); |
| 272 | |
| 273 | struct QSSGRenderCameraData |
| 274 | { |
| 275 | QMatrix4x4 viewProjection; |
| 276 | std::optional<QSSGClippingFrustum> clippingFrustum; |
| 277 | QVector3D direction { 0.0f, 0.0f, -1.0f }; |
| 278 | QVector3D position; |
| 279 | }; |
| 280 | |
| 281 | using QSSGRenderCameraList = QVarLengthArray<QSSGRenderCamera *, 2>; |
| 282 | using QSSGRenderCameraDataList = QVarLengthArray<QSSGRenderCameraData, 2>; |
| 283 | using QSSGRenderMvpArray = std::array<QMatrix4x4, 2>; // cannot be dynamic due to QSSGModelContext, must stick with 2 for now |
| 284 | |
| 285 | struct QSSGSubsetRenderable; |
| 286 | |
| 287 | // Different subsets from the same model will get the same |
| 288 | // model context so we can generate the MVP and normal matrix once |
| 289 | // and only once per subset. |
| 290 | struct QSSGModelContext |
| 291 | { |
| 292 | const QSSGRenderModel &model; |
| 293 | QSSGRenderMvpArray modelViewProjections; |
| 294 | QMatrix3x3 normalMatrix; |
| 295 | |
| 296 | QSSGModelContext(const QSSGRenderModel &inModel, |
| 297 | const QMatrix4x4 &globalTransform, |
| 298 | const QSSGRenderCameraDataList &allCameraData) |
| 299 | : model(inModel) |
| 300 | { |
| 301 | Q_ASSERT_X(allCameraData.size() <= qsizetype(modelViewProjections.size()), __FUNCTION__, "QSSGModelContext has no space for all MVPs" ); |
| 302 | int mvpCount = 0; |
| 303 | // For skinning, node's global transformation will be ignored and |
| 304 | // an identity matrix will be used for the normalMatrix |
| 305 | if (model.usesBoneTexture()) { |
| 306 | for (const QSSGRenderCameraData &cameraData : allCameraData) { |
| 307 | modelViewProjections[mvpCount++] = cameraData.viewProjection; |
| 308 | normalMatrix = QMatrix3x3(); |
| 309 | } |
| 310 | } else { |
| 311 | for (const QSSGRenderCameraData &cameraData : allCameraData) |
| 312 | QSSGRenderNode::calculateMVPAndNormalMatrix(globalTransfor: globalTransform, inViewProjection: cameraData.viewProjection, outMVP&: modelViewProjections[mvpCount++], outNormalMatrix&: normalMatrix); |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | QSSGDataRef<QSSGSubsetRenderable> subsets; |
| 317 | }; |
| 318 | |
| 319 | Q_STATIC_ASSERT(std::is_trivially_destructible<QSSGModelContext>::value); |
| 320 | |
| 321 | class QSSGRenderer; |
| 322 | class QSSGLayerRenderData; |
| 323 | struct QSSGShadowMapEntry; |
| 324 | |
| 325 | struct Q_QUICK3DRUNTIMERENDER_EXPORT QSSGSubsetRenderable : public QSSGRenderableObject |
| 326 | { |
| 327 | int reflectionProbeIndex = -1; |
| 328 | float distanceFromReflectionProbe; |
| 329 | quint32 subsetLevelOfDetail = 0; |
| 330 | QSSGShaderReflectionProbe reflectionProbe; |
| 331 | QSSGRenderer *renderer = nullptr; |
| 332 | const QSSGModelContext &modelContext; |
| 333 | const QSSGRenderSubset ⊂ |
| 334 | QRhiBuffer *instanceBuffer = nullptr; |
| 335 | float opacity; |
| 336 | const QSSGRenderGraphObject &material; |
| 337 | QSSGRenderableImage *firstImage; |
| 338 | QSSGShaderDefaultMaterialKey shaderDescription; |
| 339 | const QSSGShaderLightListView &lights; |
| 340 | |
| 341 | struct { |
| 342 | // Transient (due to the subsetRenderable being allocated using a |
| 343 | // per-frame allocator on every frame), not owned refs from the |
| 344 | // rhi-prepare step, used by the rhi-render step. |
| 345 | struct { |
| 346 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 347 | QRhiShaderResourceBindings *srb = nullptr; |
| 348 | } mainPass; |
| 349 | struct { |
| 350 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 351 | QRhiShaderResourceBindings *srb = nullptr; |
| 352 | } depthPrePass; |
| 353 | struct { |
| 354 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 355 | QRhiShaderResourceBindings *srb[6] = {}; |
| 356 | } shadowPass; |
| 357 | struct { |
| 358 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 359 | QRhiShaderResourceBindings *srb[6] = {}; |
| 360 | } reflectionPass; |
| 361 | } rhiRenderData; |
| 362 | |
| 363 | QSSGSubsetRenderable(Type type, |
| 364 | QSSGRenderableObjectFlags inFlags, |
| 365 | const QVector3D &inWorldCenterPt, |
| 366 | QSSGRenderer *rendr, |
| 367 | const QSSGRenderSubset &inSubset, |
| 368 | const QSSGModelContext &inModelContext, |
| 369 | float inOpacity, |
| 370 | quint32 inSubsetLevelOfDetail, |
| 371 | const QSSGRenderGraphObject &mat, |
| 372 | QSSGRenderableImage *inFirstImage, |
| 373 | QSSGShaderDefaultMaterialKey inShaderKey, |
| 374 | const QSSGShaderLightListView &inLights); |
| 375 | |
| 376 | [[nodiscard]] const QSSGRenderGraphObject &getMaterial() const { return material; } |
| 377 | }; |
| 378 | |
| 379 | Q_STATIC_ASSERT(std::is_trivially_destructible<QSSGSubsetRenderable>::value); |
| 380 | |
| 381 | /** |
| 382 | * A renderable that corresponds to a particles. |
| 383 | */ |
| 384 | struct Q_QUICK3DRUNTIMERENDER_EXPORT QSSGParticlesRenderable : public QSSGRenderableObject |
| 385 | { |
| 386 | QSSGRenderer *renderer = nullptr; |
| 387 | const QSSGRenderParticles &particles; |
| 388 | QSSGRenderableImage *firstImage; |
| 389 | QSSGRenderableImage *colorTable; |
| 390 | const QSSGShaderLightListView &lights; |
| 391 | float opacity; |
| 392 | |
| 393 | struct { |
| 394 | // Transient (due to the subsetRenderable being allocated using a |
| 395 | // per-frame allocator on every frame), not owned refs from the |
| 396 | // rhi-prepare step, used by the rhi-render step. |
| 397 | struct { |
| 398 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 399 | QRhiShaderResourceBindings *srb = nullptr; |
| 400 | } mainPass; |
| 401 | struct { |
| 402 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 403 | QRhiShaderResourceBindings *srb = nullptr; |
| 404 | } depthPrePass; |
| 405 | struct { |
| 406 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 407 | QRhiShaderResourceBindings *srb[6] = {}; |
| 408 | } shadowPass; |
| 409 | struct { |
| 410 | QRhiGraphicsPipeline *pipeline = nullptr; |
| 411 | QRhiShaderResourceBindings *srb[6] = {}; |
| 412 | } reflectionPass; |
| 413 | } rhiRenderData; |
| 414 | |
| 415 | QSSGParticlesRenderable(QSSGRenderableObjectFlags inFlags, |
| 416 | const QVector3D &inWorldCenterPt, |
| 417 | QSSGRenderer *rendr, |
| 418 | const QSSGRenderParticles &inParticles, |
| 419 | QSSGRenderableImage *inFirstImage, |
| 420 | QSSGRenderableImage *inColorTable, |
| 421 | const QSSGShaderLightListView &inLights, |
| 422 | float inOpacity); |
| 423 | }; |
| 424 | |
| 425 | Q_STATIC_ASSERT(std::is_trivially_destructible<QSSGParticlesRenderable>::value); |
| 426 | |
| 427 | QT_END_NAMESPACE |
| 428 | |
| 429 | #endif |
| 430 | |