| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "qssgrtutilities_p.h" |
| 5 | |
| 6 | #include "qssgqmlutilities_p.h" |
| 7 | #include "qssgscenedesc_p.h" |
| 8 | |
| 9 | #include <QtCore/qurl.h> |
| 10 | #include <QtCore/qbuffer.h> |
| 11 | |
| 12 | #include <QtGui/qimage.h> |
| 13 | #include <QtGui/qimagereader.h> |
| 14 | #include <QtGui/qimagewriter.h> |
| 15 | #include <QtGui/qquaternion.h> |
| 16 | |
| 17 | #include <QtQuick3DRuntimeRender/private/qssgrenderbuffermanager_p.h> |
| 18 | |
| 19 | QT_BEGIN_NAMESPACE |
| 20 | |
| 21 | |
| 22 | // Actually set the property on node->obj, using QMetaProperty::write() |
| 23 | void QSSGRuntimeUtils::applyPropertyValue(const QSSGSceneDesc::Node *node, QObject *o, QSSGSceneDesc::Property *property) |
| 24 | { |
| 25 | auto *obj = qobject_cast<QQuick3DObject *>(object: o ? o : node->obj); |
| 26 | if (!obj) |
| 27 | return; |
| 28 | |
| 29 | auto *metaObj = obj->metaObject(); |
| 30 | int propertyIndex = metaObj->indexOfProperty(name: property->name); |
| 31 | if (propertyIndex < 0) { |
| 32 | qWarning() << "QSSGSceneDesc: could not find property" << property->name << "in" << obj; |
| 33 | return; |
| 34 | } |
| 35 | auto metaProp = metaObj->property(index: propertyIndex); |
| 36 | QVariant value; |
| 37 | |
| 38 | auto metaId = property->value.metaType().id(); |
| 39 | auto *scene = node->scene; |
| 40 | |
| 41 | if (metaId == qMetaTypeId<QSSGSceneDesc::Node *>()) { |
| 42 | const auto *valueNode = qvariant_cast<QSSGSceneDesc::Node *>(v: property->value); |
| 43 | QObject *obj = valueNode ? valueNode->obj : nullptr; |
| 44 | value = QVariant::fromValue(value: obj); |
| 45 | } else if (metaId == qMetaTypeId<QSSGSceneDesc::Mesh *>()) { // Special handling for mesh nodes. |
| 46 | // Mesh nodes does not have an equivalent in the QtQuick3D scene, but is registered |
| 47 | // as a source property in the intermediate scene we therefore need to convert it to |
| 48 | // be a usable source url now. |
| 49 | const auto meshNode = qvariant_cast<const QSSGSceneDesc::Mesh *>(v: property->value); |
| 50 | const auto url = meshNode ? QUrl(QSSGBufferManager::runtimeMeshSourceName(assetId: node->scene->id, meshId: meshNode->idx)) : QUrl{}; |
| 51 | value = QVariant::fromValue(value: url); |
| 52 | } else if (metaId == qMetaTypeId<QUrl>()) { |
| 53 | const auto url = qvariant_cast<QUrl>(v: property->value); |
| 54 | // TODO: Use QUrl::resolved() instead?? |
| 55 | QString workingDir = scene->sourceDir; |
| 56 | const QUrl qurl = url.isValid() ? QUrl::fromUserInput(userInput: url.path(), workingDirectory: workingDir) : QUrl{}; |
| 57 | value = QVariant::fromValue(value: qurl); |
| 58 | } else if (metaId == qMetaTypeId<QSSGSceneDesc::Flag>() && property->call) { |
| 59 | // If we have a QSSGSceneDesc::Flag variant, then it came from setProperty(), and the setter function is defined. |
| 60 | const auto flag = qvariant_cast<QSSGSceneDesc::Flag>(v: property->value); |
| 61 | property->call->set(*obj, property->name, flag.value); |
| 62 | qDebug() << "Flag special case, probably shouldn't happen" << node->name << property->name << property->value << flag.value; |
| 63 | return; |
| 64 | } else { |
| 65 | value = property->value; |
| 66 | } |
| 67 | |
| 68 | if (value.metaType().id() == qMetaTypeId<QString>()) { |
| 69 | auto str = value.toString(); |
| 70 | auto propType = metaProp.metaType(); |
| 71 | if (propType.id() == qMetaTypeId<QVector3D>()) { |
| 72 | QStringList l = str.split(sep: u','); |
| 73 | if (l.length() != 3) { |
| 74 | qWarning() << "Wrong format for QVector3D:" << str; |
| 75 | } else { |
| 76 | QVector3D vec3(l.at(i: 0).toFloat(), l.at(i: 1).toFloat(), l.at(i: 2).toFloat()); |
| 77 | value = QVariant::fromValue(value: vec3); |
| 78 | } |
| 79 | } else if (propType.id() == qMetaTypeId<QVector2D>()) { |
| 80 | QStringList l = str.split(sep: u','); |
| 81 | if (l.length() != 2) { |
| 82 | qWarning() << "Wrong format for QVector2D:" << str; |
| 83 | } else { |
| 84 | QVector2D vec(l.at(i: 0).toFloat(), l.at(i: 1).toFloat()); |
| 85 | value = QVariant::fromValue(value: vec); |
| 86 | } |
| 87 | } else if (propType.id() == qMetaTypeId<QVector4D>()) { |
| 88 | QStringList l = str.split(sep: u','); |
| 89 | if (l.length() != 2) { |
| 90 | qWarning() << "Wrong format for QVector4D:" << str; |
| 91 | } else { |
| 92 | QVector4D vec(l.at(i: 0).toFloat(), l.at(i: 1).toFloat(), l.at(i: 2).toFloat(), l.at(i: 3).toFloat()); |
| 93 | value = QVariant::fromValue(value: vec); |
| 94 | } |
| 95 | } else if (propType.id() == qMetaTypeId<QQuaternion>()) { |
| 96 | QStringList l = str.split(sep: u','); |
| 97 | if (l.length() != 4) { |
| 98 | qWarning() << "Wrong format for QQuaternion:" << str; |
| 99 | } else { |
| 100 | QQuaternion quat(l.at(i: 0).toFloat(), l.at(i: 1).toFloat(), l.at(i: 2).toFloat(), l.at(i: 3).toFloat()); |
| 101 | value = QVariant::fromValue(value: quat); |
| 102 | } |
| 103 | } else { |
| 104 | // All other strings are supposed to be in QML-compatible format, so they can be written out directly |
| 105 | } |
| 106 | } else if (value.metaType().id() == qMetaTypeId<QSSGSceneDesc::NodeList*>()) { |
| 107 | auto qmlListVar = metaProp.read(obj); |
| 108 | // We have to write explicit code for each list property type, since metatype can't |
| 109 | // tell us if we have a QQmlListProperty (if we had known, we could have made a naughty |
| 110 | // hack and just static_cast to QQmlListProperty<QObject> |
| 111 | if (qmlListVar.metaType().id() == qMetaTypeId<QQmlListProperty<QQuick3DMaterial>>()) { |
| 112 | auto qmlList = qvariant_cast<QQmlListProperty<QQuick3DMaterial>>(v: qmlListVar); |
| 113 | auto nodeList = qvariant_cast<QSSGSceneDesc::NodeList*>(v: value); |
| 114 | auto head = reinterpret_cast<QSSGSceneDesc::Node **>(nodeList->head); |
| 115 | |
| 116 | for (int i = 0, end = nodeList->count; i != end; ++i) |
| 117 | qmlList.append(&qmlList, qobject_cast<QQuick3DMaterial *>(object: (*(head + i))->obj)); |
| 118 | |
| 119 | } else { |
| 120 | qWarning() << "Can't handle list property type" << qmlListVar.metaType(); |
| 121 | } |
| 122 | return; //In any case, we can't send NodeList to QMetaProperty::write() |
| 123 | } |
| 124 | |
| 125 | // qobject_cast doesn't work on nullptr, so we must convert the pointer manually |
| 126 | if ((metaProp.metaType().flags() & QMetaType::PointerToQObject) && value.isNull()) |
| 127 | value.convert(type: metaProp.metaType()); //This will return false, but convert to the type anyway |
| 128 | |
| 129 | // Q_ENUMS with '|' is explicitly handled, otherwise uses QVariant::convert. This means |
| 130 | // we get implicit qobject_cast and string to: |
| 131 | // QMetaType::Bool, QMetaType::QByteArray, QMetaType::QChar, QMetaType::QColor, QMetaType::QDate, QMetaType::QDateTime, |
| 132 | // QMetaType::Double, QMetaType::QFont, QMetaType::Int, QMetaType::QKeySequence, QMetaType::LongLong, |
| 133 | // QMetaType::QStringList, QMetaType::QTime, QMetaType::UInt, QMetaType::ULongLong, QMetaType::QUuid |
| 134 | |
| 135 | bool success = metaProp.write(obj, value); |
| 136 | |
| 137 | if (!success) { |
| 138 | qWarning() << "Failure when setting property" << property->name << "to" << property->value << "maps to" << value |
| 139 | << "property metatype:" << metaProp.typeName(); |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | |
| 144 | static void setProperties(QQuick3DObject &obj, const QSSGSceneDesc::Node &node, const QString &workingDir = {}) |
| 145 | { |
| 146 | using namespace QSSGSceneDesc; |
| 147 | const auto &properties = node.properties; |
| 148 | auto it = properties.begin(); |
| 149 | const auto end = properties.end(); |
| 150 | for (; it != end; ++it) { |
| 151 | const auto &v = *it; |
| 152 | if (!v->call) { |
| 153 | QSSGRuntimeUtils::applyPropertyValue(node: &node, o: &obj, property: v); |
| 154 | continue; |
| 155 | } |
| 156 | const auto &var = v->value; |
| 157 | if (var.metaType().id() == qMetaTypeId<Node *>()) { |
| 158 | const auto *node = qvariant_cast<Node *>(v: var); |
| 159 | v->call->set(obj, v->name, node ? node->obj : nullptr); |
| 160 | } else if (var.metaType() == QMetaType::fromType<Mesh *>()) { // Special handling for mesh nodes. |
| 161 | // Mesh nodes does not have an equivalent in the QtQuick3D scene, but is registered |
| 162 | // as a source property in the intermediate scene we therefore need to convert it to |
| 163 | // be a usable source url now. |
| 164 | const auto meshNode = qvariant_cast<const Mesh *>(v: var); |
| 165 | const auto url = meshNode ? QUrl(QSSGBufferManager::runtimeMeshSourceName(assetId: node.scene->id, meshId: meshNode->idx)) : QUrl{}; |
| 166 | v->call->set(obj, v->name, &url); |
| 167 | } else if (var.metaType() == QMetaType::fromType<QUrl>()) { |
| 168 | const auto url = qvariant_cast<QUrl>(v: var); |
| 169 | // TODO: Use QUrl::resolved() instead |
| 170 | const QUrl qurl = url.isValid() ? QUrl::fromUserInput(userInput: url.toString(), workingDirectory: workingDir) : QUrl{}; |
| 171 | v->call->set(obj, v->name, &qurl); |
| 172 | } else if (var.metaType().id() == qMetaTypeId<QSSGSceneDesc::Flag>()) { |
| 173 | const auto flag = qvariant_cast<QSSGSceneDesc::Flag>(v: var); |
| 174 | v->call->set(obj, v->name, flag.value); |
| 175 | } else { |
| 176 | v->call->set(obj, v->name, var); |
| 177 | } |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | template<typename GraphObjectType, typename NodeType> |
| 182 | GraphObjectType *createRuntimeObject(NodeType &node, QQuick3DObject &parent) |
| 183 | { |
| 184 | GraphObjectType *obj = qobject_cast<GraphObjectType *>(node.obj); |
| 185 | if (!obj) { |
| 186 | node.obj = qobject_cast<QQuick3DObject *>(obj = new GraphObjectType); |
| 187 | obj->setParent(&parent); |
| 188 | obj->setParentItem(&parent); |
| 189 | } |
| 190 | Q_ASSERT(obj == node.obj); |
| 191 | |
| 192 | return obj; |
| 193 | } |
| 194 | |
| 195 | template<> |
| 196 | QQuick3DTextureData *createRuntimeObject<QQuick3DTextureData>(QSSGSceneDesc::TextureData &node, QQuick3DObject &parent) |
| 197 | { |
| 198 | QQuick3DTextureData *obj = qobject_cast<QQuick3DTextureData *>(object: node.obj); |
| 199 | if (!obj) { |
| 200 | node.obj = qobject_cast<QQuick3DObject *>(object: obj = new QQuick3DTextureData); |
| 201 | obj->setParent(&parent); |
| 202 | obj->setParentItem(&parent); |
| 203 | |
| 204 | const auto &texData = node.data; |
| 205 | const bool isCompressed = ((node.flgs & quint8(QSSGSceneDesc::TextureData::Flags::Compressed)) != 0); |
| 206 | |
| 207 | if (!texData.isEmpty()) { |
| 208 | QImage image; |
| 209 | if (isCompressed) { |
| 210 | QByteArray data = texData; |
| 211 | QBuffer readBuffer(&data); |
| 212 | QImageReader imageReader(&readBuffer, node.fmt); |
| 213 | image = imageReader.read(); |
| 214 | if (image.isNull()) |
| 215 | qWarning() << imageReader.errorString(); |
| 216 | } else { |
| 217 | const auto &size = node.sz; |
| 218 | image = QImage(reinterpret_cast<const uchar *>(texData.data()), size.width(), size.height(), QImage::Format::Format_RGBA8888); |
| 219 | } |
| 220 | |
| 221 | if (!image.isNull()) { |
| 222 | const QPixelFormat pixFormat = image.pixelFormat(); |
| 223 | QImage::Format targetFormat = QImage::Format_RGBA8888_Premultiplied; |
| 224 | QQuick3DTextureData::Format textureFormat = QQuick3DTextureData::Format::RGBA8; |
| 225 | if (image.colorCount()) { // a palleted image |
| 226 | targetFormat = QImage::Format_RGBA8888; |
| 227 | } else if (pixFormat.channelCount() == 1) { |
| 228 | targetFormat = QImage::Format_Grayscale8; |
| 229 | textureFormat = QQuick3DTextureData::Format::R8; |
| 230 | } else if (pixFormat.alphaUsage() == QPixelFormat::IgnoresAlpha) { |
| 231 | targetFormat = QImage::Format_RGBX8888; |
| 232 | } else if (pixFormat.premultiplied() == QPixelFormat::NotPremultiplied) { |
| 233 | targetFormat = QImage::Format_RGBA8888; |
| 234 | } |
| 235 | |
| 236 | image.convertTo(f: targetFormat); // convert to a format mappable to QRhiTexture::Format |
| 237 | image.mirror(); // Flip vertically to the conventional Y-up orientation |
| 238 | |
| 239 | const auto bytes = image.sizeInBytes(); |
| 240 | obj->setSize(image.size()); |
| 241 | obj->setFormat(textureFormat); |
| 242 | obj->setTextureData(QByteArray(reinterpret_cast<const char *>(image.constBits()), bytes)); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | return obj; |
| 248 | } |
| 249 | |
| 250 | |
| 251 | // Resources may refer to other resources and/or nodes, so we first generate all the resources without setting properties, |
| 252 | // then all the nodes with properties, and finally we set properties for the resources |
| 253 | // TODO: split this into different functions |
| 254 | |
| 255 | void QSSGRuntimeUtils::createGraphObject(QSSGSceneDesc::Node &node, |
| 256 | QQuick3DObject &parent, bool traverseChildrenAndSetProperties) |
| 257 | { |
| 258 | using namespace QSSGSceneDesc; |
| 259 | |
| 260 | QQuick3DObject *obj = nullptr; |
| 261 | switch (node.nodeType) { |
| 262 | case Node::Type::Skeleton: |
| 263 | // Skeleton is a resource that also needs to be in the node tree. We don't do that anymore. |
| 264 | qWarning(msg: "Skeleton runtime import not supported" ); |
| 265 | |
| 266 | // NOTE: The skeleton is special as it's a resource and a node, the |
| 267 | // hierarchical parent is therefore important here. |
| 268 | if (!node.obj) {// 1st Phase : 'create Resources' |
| 269 | obj = createRuntimeObject<QQuick3DSkeleton>(node&: static_cast<Skeleton &>(node), parent); |
| 270 | } else { // 2nd Phase : setParent for the Node hierarchy. |
| 271 | obj = qobject_cast<QQuick3DSkeleton *>(object: node.obj); |
| 272 | obj->setParent(&parent); |
| 273 | obj->setParentItem(&parent); |
| 274 | } |
| 275 | break; |
| 276 | case Node::Type::Joint: |
| 277 | obj = createRuntimeObject<QQuick3DJoint>(node&: static_cast<Joint &>(node), parent); |
| 278 | break; |
| 279 | case Node::Type::Skin: |
| 280 | obj = createRuntimeObject<QQuick3DSkin>(node&: static_cast<Skin &>(node), parent); |
| 281 | break; |
| 282 | case Node::Type::MorphTarget: |
| 283 | obj = createRuntimeObject<QQuick3DMorphTarget>(node&: static_cast<MorphTarget &>(node), parent); |
| 284 | break; |
| 285 | case Node::Type::Light: |
| 286 | { |
| 287 | auto &light = static_cast<Light &>(node); |
| 288 | if (light.runtimeType == Node::RuntimeType::DirectionalLight) |
| 289 | obj = createRuntimeObject<QQuick3DDirectionalLight>(node&: light, parent); |
| 290 | else if (light.runtimeType == Node::RuntimeType::PointLight) |
| 291 | obj = createRuntimeObject<QQuick3DPointLight>(node&: light, parent); |
| 292 | else if (light.runtimeType == Node::RuntimeType::SpotLight) |
| 293 | obj = createRuntimeObject<QQuick3DSpotLight>(node&: light, parent); |
| 294 | else |
| 295 | Q_UNREACHABLE(); |
| 296 | } |
| 297 | break; |
| 298 | case Node::Type::Transform: |
| 299 | obj = createRuntimeObject<QQuick3DNode>(node, parent); |
| 300 | break; |
| 301 | case Node::Type::Camera: |
| 302 | { |
| 303 | auto &camera = static_cast<Camera &>(node); |
| 304 | if (camera.runtimeType == Node::RuntimeType::OrthographicCamera) |
| 305 | obj = createRuntimeObject<QQuick3DOrthographicCamera>(node&: camera, parent); |
| 306 | else if (camera.runtimeType == Node::RuntimeType::PerspectiveCamera) |
| 307 | obj = createRuntimeObject<QQuick3DPerspectiveCamera>(node&: camera, parent); |
| 308 | else if (camera.runtimeType == Node::RuntimeType::CustomCamera) |
| 309 | obj = createRuntimeObject<QQuick3DCustomCamera>(node&: camera, parent); |
| 310 | else |
| 311 | Q_UNREACHABLE(); |
| 312 | } |
| 313 | break; |
| 314 | case Node::Type::Model: |
| 315 | obj = createRuntimeObject<QQuick3DModel>(node&: static_cast<Model &>(node), parent); |
| 316 | break; |
| 317 | case Node::Type::Texture: |
| 318 | if (node.runtimeType == Node::RuntimeType::TextureData) |
| 319 | obj = createRuntimeObject<QQuick3DTextureData>(node&: static_cast<TextureData &>(node), parent); |
| 320 | else if (node.runtimeType == Node::RuntimeType::Image2D) |
| 321 | obj = createRuntimeObject<QQuick3DTexture>(node&: static_cast<Texture &>(node), parent); |
| 322 | else if (node.runtimeType == Node::RuntimeType::ImageCube) |
| 323 | obj = createRuntimeObject<QQuick3DCubeMapTexture>(node&: static_cast<Texture &>(node), parent); |
| 324 | else |
| 325 | Q_UNREACHABLE(); |
| 326 | break; |
| 327 | case Node::Type::Material: |
| 328 | { |
| 329 | if (node.runtimeType == Node::RuntimeType::PrincipledMaterial) |
| 330 | obj = createRuntimeObject<QQuick3DPrincipledMaterial>(node&: static_cast<Material &>(node), parent); |
| 331 | else if (node.runtimeType == Node::RuntimeType::CustomMaterial) |
| 332 | obj = createRuntimeObject<QQuick3DCustomMaterial>(node&: static_cast<Material &>(node), parent); |
| 333 | else if (node.runtimeType == Node::RuntimeType::SpecularGlossyMaterial) |
| 334 | obj = createRuntimeObject<QQuick3DSpecularGlossyMaterial>(node&: static_cast<Material &>(node), parent); |
| 335 | else |
| 336 | Q_UNREACHABLE(); |
| 337 | } |
| 338 | break; |
| 339 | case Node::Type::Mesh: |
| 340 | // There's no runtime object for this type, but we need to register the mesh with the |
| 341 | // buffer manager (this will happen once the mesh property is processed on the model). |
| 342 | break; |
| 343 | } |
| 344 | |
| 345 | if (obj && traverseChildrenAndSetProperties) { |
| 346 | setProperties(obj&: *obj, node); |
| 347 | for (auto &chld : node.children) |
| 348 | createGraphObject(node&: *chld, parent&: *obj); |
| 349 | } |
| 350 | } |
| 351 | |
| 352 | QQuick3DNode *QSSGRuntimeUtils::createScene(QQuick3DNode &parent, const QSSGSceneDesc::Scene &scene) |
| 353 | { |
| 354 | if (!scene.root) { |
| 355 | qWarning(msg: "Incomplete scene description (missing plugin?)" ); |
| 356 | return nullptr; |
| 357 | } |
| 358 | |
| 359 | Q_ASSERT(QQuick3DObjectPrivate::get(&parent)->sceneManager); |
| 360 | |
| 361 | QSSGBufferManager::registerMeshData(assetId: scene.id, meshData: scene.meshStorage); |
| 362 | |
| 363 | auto root = scene.root; |
| 364 | for (const auto &resource : scene.resources) |
| 365 | createGraphObject(node&: *resource, parent, traverseChildrenAndSetProperties: false); |
| 366 | |
| 367 | createGraphObject(node&: *root, parent); |
| 368 | |
| 369 | // Some resources such as Skin have properties related with the node |
| 370 | // hierarchy. Therefore, resources are handled after nodes. |
| 371 | for (const auto &resource : scene.resources) { |
| 372 | if (resource->obj != nullptr) // A mesh node has no runtime object. |
| 373 | setProperties(obj&: static_cast<QQuick3DObject &>(*resource->obj), node: *resource, workingDir: scene.sourceDir); |
| 374 | } |
| 375 | |
| 376 | // Usually it makes sense to only enable 1 timeline at a time |
| 377 | // so for now we just enable the first one. |
| 378 | bool isFirstAnimation = true; |
| 379 | for (const auto &anim: scene.animations) { |
| 380 | QSSGQmlUtilities::createTimelineAnimation(anim: *anim, parent: root->obj, isEnabled: isFirstAnimation); |
| 381 | if (isFirstAnimation) |
| 382 | isFirstAnimation = false; |
| 383 | } |
| 384 | |
| 385 | return qobject_cast<QQuick3DNode *>(object: scene.root->obj); |
| 386 | } |
| 387 | |
| 388 | QT_END_NAMESPACE |
| 389 | |