| /**************************************************************************** |
| ** |
| ** Copyright (C) 2019 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of Qt Quick 3D. |
| ** |
| ** $QT_BEGIN_LICENSE:GPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 3 or (at your option) any later version |
| ** approved by the KDE Free Qt Foundation. The licenses are as published by |
| ** the Free Software Foundation and appearing in the file LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "assimpimporter.h" |
| |
| #include <assimp/Importer.hpp> |
| #include <assimp/scene.h> |
| #include <assimp/Logger.hpp> |
| #include <assimp/DefaultLogger.hpp> |
| #include <assimp/postprocess.h> |
| #include <assimp/pbrmaterial.h> |
| |
| #include <QtQuick3DAssetImport/private/qssgmeshutilities_p.h> |
| |
| #include <QtGui/QImage> |
| #include <QtGui/QImageReader> |
| #include <QtGui/QImageWriter> |
| #include <QtGui/QQuaternion> |
| |
| #include <QtCore/QBuffer> |
| #include <QtCore/QByteArray> |
| #include <QtCore/QList> |
| #include <QtCore/QJsonDocument> |
| #include <QtCore/QJsonObject> |
| |
| #include <qmath.h> |
| |
| #include <algorithm> |
| #include <limits> |
| |
| QT_BEGIN_NAMESPACE |
| |
| #define demonPostProcessPresets ( \ |
| aiProcess_CalcTangentSpace | \ |
| aiProcess_GenSmoothNormals | \ |
| aiProcess_JoinIdenticalVertices | \ |
| aiProcess_ImproveCacheLocality | \ |
| aiProcess_LimitBoneWeights | \ |
| aiProcess_RemoveRedundantMaterials | \ |
| aiProcess_SplitLargeMeshes | \ |
| aiProcess_Triangulate | \ |
| aiProcess_GenUVCoords | \ |
| aiProcess_SortByPType | \ |
| aiProcess_FindDegenerates | \ |
| aiProcess_FindInvalidData | \ |
| 0 ) |
| |
| AssimpImporter::AssimpImporter() |
| { |
| QFile optionFile(":/assimpimporter/options.json"); |
| optionFile.open(QIODevice::ReadOnly); |
| QByteArray options = optionFile.readAll(); |
| optionFile.close(); |
| auto optionsDocument = QJsonDocument::fromJson(options); |
| m_options = optionsDocument.object().toVariantMap(); |
| m_postProcessSteps = aiPostProcessSteps(demonPostProcessPresets); |
| |
| m_importer = new Assimp::Importer(); |
| // Remove primatives that are not Triangles |
| m_importer->SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_POINT | aiPrimitiveType_LINE); |
| } |
| |
| AssimpImporter::~AssimpImporter() |
| { |
| for (auto *animation : m_animations) |
| delete animation; |
| delete m_importer; |
| } |
| |
| const QString AssimpImporter::name() const |
| { |
| return QStringLiteral("assimp"); |
| } |
| |
| const QStringList AssimpImporter::inputExtensions() const |
| { |
| QStringList extensions; |
| extensions.append(QStringLiteral("fbx")); |
| extensions.append(QStringLiteral("dae")); |
| extensions.append(QStringLiteral("obj")); |
| extensions.append(QStringLiteral("blend")); |
| extensions.append(QStringLiteral("gltf")); |
| extensions.append(QStringLiteral("glb")); |
| return extensions; |
| } |
| |
| const QString AssimpImporter::outputExtension() const |
| { |
| return QStringLiteral(".qml"); |
| } |
| |
| const QString AssimpImporter::type() const |
| { |
| return QStringLiteral("Scene"); |
| } |
| |
| const QString AssimpImporter::typeDescription() const |
| { |
| return QObject::tr("3D Scene"); |
| } |
| |
| const QVariantMap AssimpImporter::importOptions() const |
| { |
| return m_options; |
| } |
| |
| const QString AssimpImporter::import(const QString &sourceFile, const QDir &savePath, const QVariantMap &options, QStringList *generatedFiles) |
| { |
| Q_UNUSED(options) |
| |
| QString errorString; |
| m_savePath = savePath; |
| m_sourceFile = QFileInfo(sourceFile); |
| |
| // Create savePath if it doesn't exist already |
| m_savePath.mkdir("."); |
| |
| processOptions(options); |
| |
| m_scene = m_importer->ReadFile(sourceFile.toStdString(), m_postProcessSteps); |
| if (!m_scene) { |
| // Scene failed to load, use logger to get the reason |
| return QString::fromLocal8Bit(m_importer->GetErrorString()); |
| } |
| |
| // There is special handling needed for GLTF assets |
| if (m_sourceFile.completeSuffix() == QStringLiteral("gltf") || m_sourceFile.completeSuffix() == QStringLiteral("glb")) |
| m_gltfMode = true; |
| else |
| m_gltfMode = false; |
| |
| |
| // Generate Embedded Texture Sources |
| if (m_scene->mNumTextures) |
| m_savePath.mkdir(QStringLiteral("./maps")); |
| for (uint i = 0; i < m_scene->mNumTextures; ++i) { |
| aiTexture *texture = m_scene->mTextures[i]; |
| if (texture->mHeight == 0) { |
| // compressed format, try to load with Image Loader |
| QByteArray data(reinterpret_cast<char *>(texture->pcData), texture->mWidth); |
| QBuffer readBuffer(&data); |
| QByteArray format = texture->achFormatHint; |
| QImageReader imageReader(&readBuffer, format); |
| QImage image = imageReader.read(); |
| if (image.isNull()) { |
| qWarning() << imageReader.errorString(); |
| continue; |
| } |
| |
| // ### maybe dont always use png |
| const QString saveFileName = savePath.absolutePath() + |
| QStringLiteral("/maps/") + |
| QString::number(i) + |
| QStringLiteral(".png"); |
| image.save(saveFileName); |
| |
| } else { |
| // Raw format, just convert data to QImage |
| QImage rawImage(reinterpret_cast<uchar *>(texture->pcData), texture->mWidth, texture->mHeight, QImage::Format_RGBA8888); |
| const QString saveFileName = savePath.absolutePath() + |
| QStringLiteral("/maps/") + |
| QString::number(i) + |
| QStringLiteral(".png"); |
| rawImage.save(saveFileName); |
| } |
| } |
| |
| // Check for Cameras |
| if (m_scene->HasCameras()) { |
| for (uint i = 0; i < m_scene->mNumCameras; ++i) { |
| aiCamera *camera = m_scene->mCameras[i]; |
| aiNode *node = m_scene->mRootNode->FindNode(camera->mName); |
| if (camera && node) |
| m_cameras.insert(node, camera); |
| } |
| } |
| |
| // Check for Lights |
| if (m_scene->HasLights()) { |
| for (uint i = 0; i < m_scene->mNumLights; ++i) { |
| aiLight *light = m_scene->mLights[i]; |
| aiNode *node = m_scene->mRootNode->FindNode(light->mName); |
| if (light && node) |
| m_lights.insert(node, light); |
| } |
| } |
| |
| // Materials |
| |
| // Traverse Node Tree |
| |
| // Animations (timeline based) |
| if (m_scene->HasAnimations()) { |
| for (uint i = 0; i < m_scene->mNumAnimations; ++i) { |
| aiAnimation *animation = m_scene->mAnimations[i]; |
| if (!animation) |
| continue; |
| m_animations.push_back(new QHash<aiNode *, aiNodeAnim *>()); |
| for (uint j = 0; j < animation->mNumChannels; ++j) { |
| aiNodeAnim *channel = animation->mChannels[j]; |
| aiNode *node = m_scene->mRootNode->FindNode(channel->mNodeName); |
| if (channel && node) |
| m_animations.back()->insert(node, channel); |
| } |
| } |
| } |
| |
| // Create QML Component |
| QFileInfo sourceFileInfo(sourceFile); |
| |
| |
| QString targetFileName = savePath.absolutePath() + QDir::separator() + |
| QSSGQmlUtilities::qmlComponentName(sourceFileInfo.baseName()) + |
| QStringLiteral(".qml"); |
| QFile targetFile(targetFileName); |
| if (!targetFile.open(QIODevice::WriteOnly)) { |
| errorString += QString("Could not write to file: ") + targetFileName; |
| } else { |
| QTextStream output(&targetFile); |
| |
| // Imports header |
| writeHeader(output); |
| |
| // Component Code |
| processNode(m_scene->mRootNode, output); |
| |
| targetFile.close(); |
| if (generatedFiles) |
| *generatedFiles += targetFileName; |
| } |
| |
| return errorString; |
| } |
| |
| void AssimpImporter::writeHeader(QTextStream &output) |
| { |
| output << "import QtQuick3D 1.12" << endl; |
| output << "import QtQuick 2.12" << endl; |
| if (m_scene->HasAnimations()) |
| output << "import QtQuick.Timeline 1.0" << endl; |
| } |
| |
| void AssimpImporter::processNode(aiNode *node, QTextStream &output, int tabLevel) |
| { |
| aiNode *currentNode = node; |
| if (currentNode) { |
| output << endl; |
| // Figure out what kind of node this is |
| if (isModel(currentNode)) { |
| // Model |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("Model {") << endl; |
| generateModelProperties(currentNode, output, tabLevel + 1); |
| m_nodeTypeMap.insert(node, QSSGQmlUtilities::PropertyMap::Model); |
| } else if (isLight(currentNode)) { |
| // Light |
| // Light property name will be produced in the function, |
| // and then tabLevel will be increased. |
| auto type = generateLightProperties(currentNode, output, tabLevel); |
| m_nodeTypeMap.insert(node, type); |
| } else if (isCamera(currentNode)) { |
| // Camera (always assumed to be perspective for some reason) |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("PerspectiveCamera {") << endl; |
| generateCameraProperties(currentNode, output, tabLevel + 1); |
| m_nodeTypeMap.insert(node, QSSGQmlUtilities::PropertyMap::Camera); |
| } else { |
| // Transform Node |
| |
| // ### Make empty transform node removal optional |
| // Check if the node actually does something before generating it |
| // and return early without processing the rest of the branch |
| if (!containsNodesOfConsequence(node)) |
| return; |
| |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("Node {") << endl; |
| generateNodeProperties(currentNode, output, tabLevel + 1); |
| m_nodeTypeMap.insert(node, QSSGQmlUtilities::PropertyMap::Node); |
| } |
| |
| // Process All Children Nodes |
| for (uint i = 0; i < currentNode->mNumChildren; ++i) |
| processNode(currentNode->mChildren[i], output, tabLevel + 1); |
| |
| if (tabLevel == 0) |
| processAnimations(output); |
| |
| // Write the QML Footer |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("}") << endl; |
| } |
| } |
| |
| void AssimpImporter::generateModelProperties(aiNode *modelNode, QTextStream &output, int tabLevel) |
| { |
| generateNodeProperties(modelNode, output, tabLevel); |
| |
| // source |
| // Combine all the meshes referenced by this model into a single MultiMesh file |
| QVector<aiMesh *> meshes; |
| QVector<aiMaterial *> materials; |
| for (uint i = 0; i < modelNode->mNumMeshes; ++i) { |
| aiMesh *mesh = m_scene->mMeshes[modelNode->mMeshes[i]]; |
| aiMaterial *material = m_scene->mMaterials[mesh->mMaterialIndex]; |
| meshes.append(mesh); |
| materials.append(material); |
| } |
| |
| // Model name can contain invalid characters for filename, so just to be safe, convert the name |
| // into qml id first. |
| QString modelName = QString::fromUtf8(modelNode->mName.C_Str()); |
| modelName = QSSGQmlUtilities::sanitizeQmlId(modelName); |
| |
| QString outputMeshFile = QStringLiteral("meshes/") + modelName + QStringLiteral(".mesh"); |
| |
| m_savePath.mkdir(QStringLiteral("./meshes")); |
| QString meshFilePath = m_savePath.absolutePath() + QLatin1Char('/') + outputMeshFile; |
| int index = 0; |
| while (m_generatedFiles.contains(meshFilePath)) { |
| outputMeshFile = QStringLiteral("meshes/%1_%2.mesh").arg(modelName).arg(++index); |
| meshFilePath = m_savePath.absolutePath() + QLatin1Char('/') + outputMeshFile; |
| } |
| QFile meshFile(meshFilePath); |
| if (generateMeshFile(meshFile, meshes).isEmpty()) |
| m_generatedFiles << meshFilePath; |
| |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << "source: \"" << outputMeshFile |
| << QStringLiteral("\"") << endl; |
| |
| // skeletonRoot |
| |
| // materials |
| // If there are any new materials, add them as children of the Model first |
| for (int i = 0; i < materials.count(); ++i) { |
| if (!m_materialIdMap.contains(materials[i])) { |
| generateMaterial(materials[i], output, tabLevel); |
| output << endl; |
| } |
| } |
| |
| // For each sub-mesh, generate a material reference for this list |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << "materials: [" << endl; |
| for (int i = 0; i < materials.count(); ++i) { |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << m_materialIdMap[materials[i]]; |
| if (i < materials.count() - 1) |
| output << QStringLiteral(","); |
| output << endl; |
| } |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << "]" << endl; |
| } |
| |
| QSSGQmlUtilities::PropertyMap::Type AssimpImporter::generateLightProperties(aiNode *lightNode, QTextStream &output, int tabLevel) |
| { |
| aiLight *light = m_lights.value(lightNode); |
| // We assume that the direction vector for a light is (0, 0, 1) |
| // so if the direction vector is non-null, but not (0, 0, 1) we |
| // need to correct the translation |
| aiMatrix4x4 correctionMatrix; |
| if (light->mDirection != aiVector3D(0, 0, 0)) { |
| if (light->mDirection != aiVector3D(0, 0, 1)) { |
| aiMatrix4x4::FromToMatrix(light->mDirection, aiVector3D(0, 0, 1), correctionMatrix); |
| } |
| } |
| |
| |
| // lightType |
| QSSGQmlUtilities::PropertyMap::Type lightType; |
| if (light->mType == aiLightSource_DIRECTIONAL || light->mType == aiLightSource_AMBIENT ) { |
| lightType = QSSGQmlUtilities::PropertyMap::DirectionalLight; |
| output << QSSGQmlUtilities::insertTabs(tabLevel++) << QStringLiteral("DirectionalLight {") << endl; |
| } else if (light->mType == aiLightSource_POINT) { |
| lightType = QSSGQmlUtilities::PropertyMap::PointLight; |
| output << QSSGQmlUtilities::insertTabs(tabLevel++) << QStringLiteral("PointLight {") << endl; |
| } else if (light->mType == aiLightSource_AREA) { |
| lightType = QSSGQmlUtilities::PropertyMap::AreaLight; |
| output << QSSGQmlUtilities::insertTabs(tabLevel++) << QStringLiteral("AreaLight {") << endl; |
| } else { |
| // We dont know what it is, assume its a point light |
| lightType = QSSGQmlUtilities::PropertyMap::PointLight; |
| output << QSSGQmlUtilities::insertTabs(tabLevel++) << QStringLiteral("PointLight {") << endl; |
| } |
| |
| generateNodeProperties(lightNode, output, tabLevel, correctionMatrix, true); |
| |
| // diffuseColor |
| QColor diffuseColor = QColor::fromRgbF(light->mColorDiffuse.r, light->mColorDiffuse.g, light->mColorDiffuse.b); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("color"), diffuseColor); |
| |
| // ambientColor |
| if (light->mType == aiLightSource_AMBIENT) { |
| // We only want ambient light color if it is explicit |
| QColor ambientColor = QColor::fromRgbF(light->mColorAmbient.r, light->mColorAmbient.g, light->mColorAmbient.b); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("ambientColor"), ambientColor); |
| } |
| // brightness |
| // Its default value is 100 and the normalized value 1 will be used. |
| |
| if (light->mType == aiLightSource_POINT) { |
| // constantFade |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("constantFade"), light->mAttenuationConstant); |
| |
| // linearFade |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("linearFade"), light->mAttenuationLinear); |
| |
| // exponentialFade |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("quadraticFade"), light->mAttenuationQuadratic); |
| } |
| |
| if (light->mType == aiLightSource_AREA) { |
| // areaWidth |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("width"), light->mSize.x); |
| |
| // areaHeight |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, lightType, QStringLiteral("height"), light->mSize.y); |
| } |
| |
| // castShadow |
| |
| // shadowBias |
| |
| // shadowFactor |
| |
| // shadowMapResolution |
| |
| // shadowMapFar |
| |
| // shadowMapFieldOfView |
| |
| // shadowFilter |
| |
| return lightType; |
| } |
| |
| void AssimpImporter::generateCameraProperties(aiNode *cameraNode, QTextStream &output, int tabLevel) |
| { |
| aiCamera *camera = m_cameras.value(cameraNode); |
| |
| // We assume these default forward and up vectors, so if this isn't |
| // the case we have to do additional transform |
| aiMatrix4x4 correctionMatrix; |
| if (camera->mLookAt != aiVector3D(0, 0, 1)) |
| { |
| aiMatrix4x4 lookAtCorrection; |
| aiMatrix4x4::FromToMatrix(camera->mLookAt, aiVector3D(0, 0, 1), lookAtCorrection); |
| correctionMatrix *= lookAtCorrection; |
| } |
| |
| if (camera->mUp != aiVector3D(0, 1, 0)) { |
| aiMatrix4x4 upCorrection; |
| aiMatrix4x4::FromToMatrix(camera->mUp, aiVector3D(0, 1, 0), upCorrection); |
| correctionMatrix *= upCorrection; |
| } |
| |
| generateNodeProperties(cameraNode, output, tabLevel, correctionMatrix, true); |
| |
| // clipNear |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, QSSGQmlUtilities::PropertyMap::Camera, QStringLiteral("clipNear"), camera->mClipPlaneNear); |
| |
| // clipFar |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, QSSGQmlUtilities::PropertyMap::Camera, QStringLiteral("clipFar"), camera->mClipPlaneFar); |
| |
| // fieldOfView |
| float fov = qRadiansToDegrees(camera->mHorizontalFOV); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, QSSGQmlUtilities::PropertyMap::Camera, QStringLiteral("fieldOfView"), fov); |
| |
| // isFieldOfViewHorizontal |
| QSSGQmlUtilities::writeQmlPropertyHelper(output,tabLevel, QSSGQmlUtilities::PropertyMap::Camera, QStringLiteral("fieldOfViewOrientation"), "Camera.Horizontal"); |
| |
| // projectionMode |
| |
| // scaleMode |
| |
| // scaleAnchor |
| |
| // frustomScaleX |
| |
| // frustomScaleY |
| |
| } |
| |
| void AssimpImporter::generateNodeProperties(aiNode *node, QTextStream &output, int tabLevel, const aiMatrix4x4 &transformCorrection, bool skipScaling) |
| { |
| // id |
| QString name = QString::fromUtf8(node->mName.C_Str()); |
| if (!name.isEmpty()) { |
| // ### we may need to account of non-unique and empty names |
| QString id = generateUniqueId(QSSGQmlUtilities::sanitizeQmlId(name)); |
| m_nodeIdMap.insert(node, id); |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("id: ") << id << endl; |
| } |
| |
| // Apply correction if necessary |
| aiMatrix4x4 transformMatrix = node->mTransformation; |
| if (!transformCorrection.IsIdentity()) |
| transformMatrix *= transformCorrection; |
| |
| // Decompose Transform Matrix to get properties |
| aiVector3D scaling; |
| aiVector3D rotation; |
| aiVector3D translation; |
| transformMatrix.Decompose(scaling, rotation, translation); |
| |
| // translate |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("x"), translation.x); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("y"), translation.y); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("z"), translation.z); |
| |
| // rotation |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("rotation.x"), qRadiansToDegrees(rotation.x)); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("rotation.y"), qRadiansToDegrees(rotation.y)); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("rotation.z"), qRadiansToDegrees(rotation.z)); |
| |
| // scale |
| if (!skipScaling) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("scale.x"), scaling.x); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("scale.y"), scaling.y); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("scale.z"), scaling.z); |
| } |
| // pivot |
| |
| // opacity |
| |
| // boneid |
| |
| // rotation order |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("rotationOrder"), QStringLiteral("Node.XYZr")); |
| |
| // orientation |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, tabLevel, QSSGQmlUtilities::PropertyMap::Node, QStringLiteral("orientation"), QStringLiteral("Node.RightHanded")); |
| |
| // visible |
| |
| } |
| |
| QString AssimpImporter::generateMeshFile(QIODevice &file, const QVector<aiMesh *> &meshes) |
| { |
| if (!file.open(QIODevice::WriteOnly)) |
| return QStringLiteral("Could not open device to write mesh file"); |
| |
| |
| auto meshBuilder = QSSGMeshUtilities::QSSGMeshBuilder::createMeshBuilder(); |
| |
| struct SubsetEntryData { |
| QString name; |
| int indexLength; |
| int indexOffset; |
| }; |
| |
| // Check if we need placeholders in certain channels |
| bool needsPositionData = false; |
| bool needsNormalData = false; |
| bool needsUV1Data = false; |
| bool needsUV2Data = false; |
| bool needsTangentData = false; |
| bool needsVertexColorData = false; |
| unsigned uv1Components = 0; |
| unsigned uv2Components = 0; |
| unsigned totalVertices = 0; |
| for (const auto *mesh : meshes) { |
| totalVertices += mesh->mNumVertices; |
| uv1Components = qMax(mesh->mNumUVComponents[0], uv1Components); |
| uv2Components = qMax(mesh->mNumUVComponents[1], uv2Components); |
| needsPositionData |= mesh->HasPositions(); |
| needsNormalData |= mesh->HasNormals(); |
| needsUV1Data |= mesh->HasTextureCoords(0); |
| needsUV2Data |= mesh->HasTextureCoords(1); |
| needsTangentData |= mesh->HasTangentsAndBitangents(); |
| needsVertexColorData |=mesh->HasVertexColors(0); |
| } |
| |
| QByteArray positionData; |
| QByteArray normalData; |
| QByteArray uv1Data; |
| QByteArray uv2Data; |
| QByteArray tangentData; |
| QByteArray binormalData; |
| QByteArray vertexColorData; |
| QByteArray indexBufferData; |
| QVector<SubsetEntryData> subsetData; |
| quint32 baseIndex = 0; |
| QSSGRenderComponentType indexType = QSSGRenderComponentType::UnsignedInteger32; |
| if ((totalVertices / 3) > std::numeric_limits<quint16>::max()) |
| indexType = QSSGRenderComponentType::UnsignedInteger32; |
| |
| for (const auto *mesh : meshes) { |
| // Position |
| if (mesh->HasPositions()) |
| positionData += QByteArray(reinterpret_cast<char*>(mesh->mVertices), mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32)); |
| else if (needsPositionData) |
| positionData += QByteArray(mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| |
| // Normal |
| if (mesh->HasNormals()) |
| normalData += QByteArray(reinterpret_cast<char*>(mesh->mNormals), mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32)); |
| else if (needsNormalData) |
| normalData += QByteArray(mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| |
| // UV1 |
| if (mesh->HasTextureCoords(0)) { |
| QVector<float> uvCoords; |
| uvCoords.resize(uv1Components * mesh->mNumVertices); |
| for (uint i = 0; i < mesh->mNumVertices; ++i) { |
| int offset = i * uv1Components; |
| aiVector3D *textureCoords = mesh->mTextureCoords[0]; |
| uvCoords[offset] = textureCoords[i].x; |
| uvCoords[offset + 1] = textureCoords[i].y; |
| if (uv1Components == 3) |
| uvCoords[offset + 2] = textureCoords[i].z; |
| } |
| uv1Data += QByteArray(reinterpret_cast<const char*>(uvCoords.constData()), uvCoords.size() * sizeof(float)); |
| } else { |
| uv1Data += QByteArray(mesh->mNumVertices * uv1Components * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| } |
| |
| // UV2 |
| if (mesh->HasTextureCoords(1)) { |
| QVector<float> uvCoords; |
| uvCoords.resize(uv2Components * mesh->mNumVertices); |
| for (uint i = 0; i < mesh->mNumVertices; ++i) { |
| int offset = i * uv2Components; |
| aiVector3D *textureCoords = mesh->mTextureCoords[1]; |
| uvCoords[offset] = textureCoords[i].x; |
| uvCoords[offset + 1] = textureCoords[i].y; |
| if (uv2Components == 3) |
| uvCoords[offset + 2] = textureCoords[i].z; |
| } |
| uv2Data += QByteArray(reinterpret_cast<const char*>(uvCoords.constData()), uvCoords.size() * sizeof(float)); |
| } else { |
| uv2Data += QByteArray(mesh->mNumVertices * uv2Components * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| } |
| |
| if (mesh->HasTangentsAndBitangents()) { |
| // Tangents |
| tangentData += QByteArray(reinterpret_cast<char*>(mesh->mTangents), mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32)); |
| // Binormals (They are actually supposed to be Bitangents despite what they are called) |
| binormalData += QByteArray(reinterpret_cast<char*>(mesh->mBitangents), mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32)); |
| } else if (needsTangentData) { |
| tangentData += QByteArray(mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| binormalData += QByteArray(mesh->mNumVertices * 3 * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| } |
| // ### Bones + Weights |
| |
| // Color |
| if (mesh->HasVertexColors(0)) |
| vertexColorData += QByteArray(reinterpret_cast<char*>(mesh->mColors[0]), mesh->mNumVertices * 4 * getSizeOfType(QSSGRenderComponentType::Float32)); |
| else if (needsVertexColorData) |
| vertexColorData += QByteArray(mesh->mNumVertices * 4 * getSizeOfType(QSSGRenderComponentType::Float32), '\0'); |
| // Index Buffer |
| QVector<quint32> indexes; |
| indexes.reserve(mesh->mNumFaces * 3); |
| |
| for (unsigned int faceIndex = 0;faceIndex < mesh->mNumFaces; ++faceIndex) { |
| const auto face = mesh->mFaces[faceIndex]; |
| // Faces should always have 3 indicides |
| Q_ASSERT(face.mNumIndices == 3); |
| indexes.append(quint32(face.mIndices[0]) + baseIndex); |
| indexes.append(quint32(face.mIndices[1]) + baseIndex); |
| indexes.append(quint32(face.mIndices[2]) + baseIndex); |
| } |
| // Since we might be combining multiple meshes together, we also need to change the index offset |
| baseIndex = *std::max_element(indexes.constBegin(), indexes.constEnd()) + 1; |
| |
| SubsetEntryData subsetEntry; |
| subsetEntry.indexOffset = indexBufferData.length() / getSizeOfType(indexType); |
| subsetEntry.indexLength = indexes.length(); |
| if (indexType == QSSGRenderComponentType::UnsignedInteger32) { |
| indexBufferData += QByteArray(reinterpret_cast<const char *>(indexes.constData()), indexes.length() * getSizeOfType(indexType)); |
| } else { |
| // convert data to quint16 |
| QVector<quint16> shortIndexes; |
| shortIndexes.resize(indexes.length()); |
| for (int i = 0; i < shortIndexes.length(); ++i) |
| shortIndexes[i] = quint16(indexes[i]); |
| indexBufferData += QByteArray(reinterpret_cast<const char *>(shortIndexes.constData()), shortIndexes.length() * getSizeOfType(indexType)); |
| } |
| |
| // Subset |
| subsetEntry.name = QString::fromUtf8(m_scene->mMaterials[mesh->mMaterialIndex]->GetName().C_Str()); |
| subsetData.append(subsetEntry); |
| } |
| |
| // Vertex Buffer Entries |
| QVector<QSSGMeshUtilities::MeshBuilderVBufEntry> entries; |
| if (positionData.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry positionAttribute( QSSGMeshUtilities::Mesh::getPositionAttrName(), |
| positionData, |
| QSSGRenderComponentType::Float32, |
| 3); |
| entries.append(positionAttribute); |
| } |
| if (normalData.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry normalAttribute( QSSGMeshUtilities::Mesh::getNormalAttrName(), |
| normalData, |
| QSSGRenderComponentType::Float32, |
| 3); |
| entries.append(normalAttribute); |
| } |
| if (uv1Data.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry uv1Attribute( QSSGMeshUtilities::Mesh::getUVAttrName(), |
| uv1Data, |
| QSSGRenderComponentType::Float32, |
| uv1Components); |
| entries.append(uv1Attribute); |
| } |
| if (uv2Data.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry uv2Attribute( QSSGMeshUtilities::Mesh::getUV2AttrName(), |
| uv2Data, |
| QSSGRenderComponentType::Float32, |
| uv2Components); |
| entries.append(uv2Attribute); |
| } |
| |
| if (tangentData.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry tangentsAttribute( QSSGMeshUtilities::Mesh::getTexTanAttrName(), |
| tangentData, |
| QSSGRenderComponentType::Float32, |
| 3); |
| entries.append(tangentsAttribute); |
| } |
| |
| if (binormalData.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry binormalAttribute( QSSGMeshUtilities::Mesh::getTexBinormalAttrName(), |
| binormalData, |
| QSSGRenderComponentType::Float32, |
| 3); |
| entries.append(binormalAttribute); |
| } |
| |
| if (vertexColorData.length() > 0) { |
| QSSGMeshUtilities::MeshBuilderVBufEntry vertexColorAttribute( QSSGMeshUtilities::Mesh::getColorAttrName(), |
| vertexColorData, |
| QSSGRenderComponentType::Float32, |
| 4); |
| entries.append(vertexColorAttribute); |
| } |
| |
| meshBuilder->setVertexBuffer(entries); |
| meshBuilder->setIndexBuffer(indexBufferData, indexType); |
| |
| // Subsets |
| for (const auto &subset : subsetData) |
| meshBuilder->addMeshSubset(reinterpret_cast<const char16_t *>(subset.name.utf16()), |
| subset.indexLength, |
| subset.indexOffset, |
| 0); |
| |
| |
| |
| auto &outputMesh = meshBuilder->getMesh(); |
| outputMesh.saveMulti(file, 0); |
| |
| file.close(); |
| return QString(); |
| } |
| |
| namespace { |
| |
| QColor aiColorToQColor(const aiColor3D &color) |
| { |
| return QColor::fromRgbF(double(color.r), double(color.g), double(color.b)); |
| } |
| |
| QColor aiColorToQColor(const aiColor4D &color) |
| { |
| QColor qtColor; |
| qtColor.setRedF(double(color.r)); |
| qtColor.setGreenF(double(color.g)); |
| qtColor.setBlueF(double(color.b)); |
| qtColor.setAlphaF(double(color.a)); |
| return qtColor; |
| } |
| |
| } |
| |
| void AssimpImporter::generateMaterial(aiMaterial *material, QTextStream &output, int tabLevel) |
| { |
| output << endl; |
| if (!m_gltfMode) |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("DefaultMaterial {") << endl; |
| else |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("PrincipledMaterial {") << endl; |
| |
| // id |
| QString id = generateUniqueId(QSSGQmlUtilities::sanitizeQmlId(material->GetName().C_Str() + QStringLiteral("_material"))); |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("id: ") << id << endl; |
| m_materialIdMap.insert(material, id); |
| |
| aiReturn result; |
| |
| if (!m_gltfMode) { |
| |
| int shadingModel = 0; |
| result = material->Get(AI_MATKEY_SHADING_MODEL, shadingModel); |
| // lighting |
| if (result == aiReturn_SUCCESS) { |
| if (shadingModel == aiShadingMode_NoShading) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("lighting: DefaultMaterial.NoLighting") << endl; |
| } |
| |
| |
| QString diffuseMapImage = generateImage(material, aiTextureType_DIFFUSE, 0, tabLevel + 1); |
| if (!diffuseMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("diffuseMap: ") << diffuseMapImage << endl; |
| |
| // For some reason the normal behavior is that either you have a diffuseMap[s] or a diffuse color |
| // but no a mix of both... So only set the diffuse color if none of the diffuse maps are set: |
| if (diffuseMapImage.isNull()) { |
| aiColor3D diffuseColor; |
| result = material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::DefaultMaterial, |
| QStringLiteral("diffuseColor"), |
| aiColorToQColor(diffuseColor)); |
| } |
| } |
| |
| QString emissiveMapImage = generateImage(material, aiTextureType_EMISSIVE, 0, tabLevel + 1); |
| if (!emissiveMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("emissiveMap: ") << emissiveMapImage << endl; |
| |
| // emissiveColor AI_MATKEY_COLOR_EMISSIVE |
| aiColor3D emissiveColor; |
| result = material->Get(AI_MATKEY_COLOR_EMISSIVE, emissiveColor); |
| if (result == aiReturn_SUCCESS) { |
| // ### set emissive color |
| } |
| // specularReflectionMap |
| |
| QString specularMapImage = generateImage(material, aiTextureType_SPECULAR, 0, tabLevel + 1); |
| if (!specularMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("specularMap: ") << specularMapImage << endl; |
| |
| // specularModel AI_MATKEY_SHADING_MODEL |
| |
| // specularTint AI_MATKEY_COLOR_SPECULAR |
| aiColor3D specularTint; |
| result = material->Get(AI_MATKEY_COLOR_SPECULAR, specularTint); |
| if (result == aiReturn_SUCCESS) { |
| // ### set specular color |
| } |
| |
| // indexOfRefraction AI_MATKEY_REFRACTI |
| |
| // fresnelPower |
| |
| // specularAmount |
| |
| // specularRoughness |
| |
| // roughnessMap |
| |
| // opacity AI_MATKEY_OPACITY |
| ai_real opacity; |
| result = material->Get(AI_MATKEY_OPACITY, opacity); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::DefaultMaterial, |
| QStringLiteral("opacity"), |
| opacity); |
| } |
| |
| // opacityMap aiTextureType_OPACITY 0 |
| QString opacityMapImage = generateImage(material, aiTextureType_OPACITY, 0, tabLevel + 1); |
| if (!opacityMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("opacityMap: ") << opacityMapImage; |
| |
| // bumpMap aiTextureType_HEIGHT 0 |
| QString bumpMapImage = generateImage(material, aiTextureType_HEIGHT, 0, tabLevel + 1); |
| if (!bumpMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("bumpMap: ") << bumpMapImage; |
| |
| // bumpAmount AI_MATKEY_BUMPSCALING |
| |
| // normalMap aiTextureType_NORMALS 0 |
| QString normalMapImage = generateImage(material, aiTextureType_NORMALS, 0, tabLevel + 1); |
| if (!normalMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("normalMap: ") << normalMapImage; |
| |
| // translucencyMap |
| |
| // translucentFalloff AI_MATKEY_TRANSPARENCYFACTOR |
| |
| // diffuseLightWrap |
| |
| // (enable) vertexColors |
| |
| // displacementMap aiTextureType_DISPLACEMENT 0 |
| QString displacementMapImage = generateImage(material, aiTextureType_DISPLACEMENT, 0, tabLevel + 1); |
| if (!displacementMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("displacementMap: ") << displacementMapImage; |
| |
| // displacementAmount |
| } else { |
| // GLTF Mode |
| { |
| aiColor4D baseColorFactor; |
| result = material->Get(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_FACTOR, baseColorFactor); |
| if (result == aiReturn_SUCCESS) |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("baseColor"), |
| aiColorToQColor(baseColorFactor)); |
| |
| QString baseColorImage = generateImage(material, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_BASE_COLOR_TEXTURE, tabLevel + 1); |
| if (!baseColorImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("baseColorMap: ") << baseColorImage << endl; |
| } |
| |
| { |
| QString metalicRoughnessImage = generateImage(material, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE, tabLevel + 1); |
| if (!metalicRoughnessImage.isNull()) { |
| // there are two fields now for this, so just use it twice for now |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("metalnessMap: ") << metalicRoughnessImage << endl; |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("roughnessMap: ") << metalicRoughnessImage << endl; |
| } |
| |
| ai_real metallicFactor; |
| result = material->Get(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLIC_FACTOR, metallicFactor); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("metalness"), |
| metallicFactor); |
| } |
| |
| ai_real roughnessFactor; |
| result = material->Get(AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_ROUGHNESS_FACTOR, roughnessFactor); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("roughness"), |
| roughnessFactor); |
| } |
| } |
| |
| { |
| QString normalTextureImage = generateImage(material, aiTextureType_NORMALS, 0, tabLevel + 1); |
| if (!normalTextureImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("normalMap: ") << normalTextureImage << endl; |
| } |
| |
| // Occlusion Textures are not implimented (yet) |
| { |
| QString occlusionTextureImage = generateImage(material, aiTextureType_LIGHTMAP, 0, tabLevel + 1); |
| if (!occlusionTextureImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("occlusionMap: ") << occlusionTextureImage << endl; |
| } |
| |
| { |
| QString emissiveTextureImage = generateImage(material, aiTextureType_EMISSIVE, 0, tabLevel + 1); |
| if (!emissiveTextureImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("emissiveMap: ") << emissiveTextureImage << endl; |
| } |
| |
| { |
| aiColor3D emissiveColorFactor; |
| result = material->Get(AI_MATKEY_COLOR_EMISSIVE, emissiveColorFactor); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("emissiveColor"), |
| aiColorToQColor(emissiveColorFactor)); |
| } |
| } |
| |
| { |
| bool isDoubleSided; |
| result = material->Get(AI_MATKEY_TWOSIDED, isDoubleSided); |
| if (result == aiReturn_SUCCESS) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("cullingMode: Material.DisableCulling") << endl; |
| } |
| |
| { |
| aiString alphaMode; |
| result = material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode); |
| if (result == aiReturn_SUCCESS) { |
| const QString mode = QString::fromUtf8(alphaMode.C_Str()).toLower(); |
| QString qtMode; |
| if (mode == QStringLiteral("opaque")) |
| qtMode = QStringLiteral("PrincipledMaterial.Opaque"); |
| else if (mode == QStringLiteral("mask")) |
| qtMode = QStringLiteral("PrincipledMaterial.Mask"); |
| else if (mode == QStringLiteral("blend")) |
| qtMode = QStringLiteral("PrincipledMaterial.Blend"); |
| |
| if (!qtMode.isNull()) |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("alphaMode"), |
| qtMode); |
| |
| } |
| } |
| |
| { |
| ai_real alphaCutoff; |
| result = material->Get(AI_MATKEY_GLTF_ALPHACUTOFF, alphaCutoff); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("alphaCutoff"), |
| alphaCutoff); |
| } |
| } |
| |
| { |
| bool isUnlit; |
| result = material->Get(AI_MATKEY_GLTF_UNLIT, isUnlit); |
| if (result == aiReturn_SUCCESS && isUnlit) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("lighting: PrincipledMaterial.NoLighting") << endl; |
| } |
| |
| // SpecularGlossiness Properties |
| bool hasSpecularGlossiness; |
| result = material->Get(AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS, hasSpecularGlossiness); |
| if (result == aiReturn_SUCCESS && hasSpecularGlossiness) { |
| |
| // diffuseFactor (color) // not used (yet), but ends up being diffuseColor |
| // { |
| // aiColor4D diffuseColor; |
| // result = material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor); |
| // if (result == aiReturn_SUCCESS) |
| // QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| // tabLevel + 1, |
| // QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| // QStringLiteral("diffuseColor"), |
| // aiColorToQColor(diffuseColor)); |
| // } |
| |
| // specularColor (color) (our property is a float?) |
| // { |
| // aiColor3D specularColor; |
| // result = material->Get(AI_MATKEY_COLOR_SPECULAR, specularColor); |
| // if (result == aiReturn_SUCCESS) |
| // QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| // tabLevel + 1, |
| // QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| // QStringLiteral("specularTint"), |
| // aiColorToQColor(specularColor)); |
| // } |
| |
| // glossinessFactor (float) |
| { |
| ai_real glossiness; |
| result = material->Get(AI_MATKEY_GLTF_PBRSPECULARGLOSSINESS_GLOSSINESS_FACTOR, glossiness); |
| if (result == aiReturn_SUCCESS) |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::PrincipledMaterial, |
| QStringLiteral("specularAmount"), |
| glossiness); |
| } |
| |
| // diffuseTexture // not used (yet), but ends up being diffuseMap(1) |
| // { |
| // QString diffuseMapImage = generateImage(material, aiTextureType_DIFFUSE, 0, tabLevel + 1); |
| // if (!diffuseMapImage.isNull()) |
| // output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("diffuseMap: ") << diffuseMapImage << endl; |
| // } |
| |
| // specularGlossinessTexture |
| { |
| QString specularMapImage = generateImage(material, aiTextureType_SPECULAR, 0, tabLevel + 1); |
| if (!specularMapImage.isNull()) |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("specularMap: ") << specularMapImage << endl; |
| } |
| } |
| } |
| |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("}"); |
| } |
| |
| namespace { |
| QString aiTilingMode(int tilingMode) { |
| if (tilingMode == aiTextureMapMode_Wrap) |
| return QStringLiteral("Texture.Repeat"); |
| if (tilingMode == aiTextureMapMode_Mirror) |
| return QStringLiteral("Texture.Mirror"); |
| |
| return QStringLiteral("Texture.ClampToEdge"); |
| } |
| } |
| |
| QString AssimpImporter::generateImage(aiMaterial *material, aiTextureType textureType, unsigned index, int tabLevel) |
| { |
| // Figure out if there is actually something to generate |
| aiString texturePath; |
| material->Get(AI_MATKEY_TEXTURE(textureType, index), texturePath); |
| // If there is no texture, then there is nothing to generate |
| if (texturePath.length == 0) |
| return QString(); |
| QString texture = QString::fromUtf8(texturePath.C_Str()); |
| // Replace Windows separator to Unix separator |
| // so that assets including Windows relative path can be converted on Unix. |
| texture.replace("\\","/"); |
| QString targetFileName; |
| // Is this an embedded texture or a file |
| if (texture.startsWith("*")) { |
| // Embedded Texture (already exists) |
| texture.remove(0, 1); |
| targetFileName = QStringLiteral("maps/") + texture + QStringLiteral(".png"); |
| } else { |
| // File Reference (needs to be copied into component) |
| // Check that this file exists |
| QString sourcePath(m_sourceFile.absolutePath() + "/" + texture); |
| QFileInfo sourceFile(sourcePath); |
| // If it doesn't exist, there is nothing to generate |
| if (!sourceFile.exists()) { |
| qWarning() << sourcePath << " (a.k.a. " << sourceFile.absoluteFilePath() << ")" |
| << " does not exist, skipping"; |
| return QString(); |
| } |
| targetFileName = QStringLiteral("maps/") + sourceFile.fileName(); |
| // Copy the file to the maps directory |
| m_savePath.mkdir(QStringLiteral("./maps")); |
| QFileInfo targetFile = m_savePath.absolutePath() + QDir::separator() + targetFileName; |
| if (QFile::copy(sourceFile.absoluteFilePath(), targetFile.absoluteFilePath())) |
| m_generatedFiles += targetFile.absoluteFilePath(); |
| } |
| // Start QML generation |
| QString outputString; |
| QTextStream output(&outputString, QIODevice::WriteOnly); |
| output << QStringLiteral("Texture {") << endl; |
| |
| output << QSSGQmlUtilities::insertTabs(tabLevel + 1) << QStringLiteral("source: \"") << targetFileName << QStringLiteral("\"") << endl; |
| |
| // mapping |
| int textureMapping; |
| aiReturn result = material->Get(AI_MATKEY_MAPPING(textureType, index), textureMapping); |
| if (result == aiReturn_SUCCESS) { |
| if (textureMapping == aiTextureMapping_UV) { |
| // So we should be able to always hit this case by passing the right flags |
| // at import. |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("mappingMode"), |
| QStringLiteral("Texture.Normal")); |
| // It would be possible to use another channel than UV0 to map texture data |
| // but for now we force everything to use UV0 |
| //int uvSource; |
| //material->Get(AI_MATKEY_UVWSRC(textureType, index), uvSource); |
| } else if (textureMapping == aiTextureMapping_SPHERE) { |
| // (not supported) |
| } else if (textureMapping == aiTextureMapping_CYLINDER) { |
| // (not supported) |
| } else if (textureMapping == aiTextureMapping_BOX) { |
| // (not supported) |
| } else if (textureMapping == aiTextureMapping_PLANE) { |
| // (not supported) |
| } else { |
| // other... (not supported) |
| } |
| } |
| |
| // mapping mode U |
| int mappingModeU; |
| result = material->Get(AI_MATKEY_MAPPINGMODE_U(textureType, index), mappingModeU); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("tilingModeHorizontal"), |
| aiTilingMode(mappingModeU)); |
| } else { |
| // import formats seem to think repeat is the default |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("tilingModeHorizontal"), |
| QStringLiteral("Texture.Repeat")); |
| } |
| |
| // mapping mode V |
| int mappingModeV; |
| result = material->Get(AI_MATKEY_MAPPINGMODE_V(textureType, index), mappingModeV); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("tilingModeVertical"), |
| aiTilingMode(mappingModeV)); |
| } else { |
| // import formats seem to think repeat is the default |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("tilingModeVertical"), |
| QStringLiteral("Texture.Repeat")); |
| } |
| |
| aiUVTransform transforms; |
| result = material->Get(AI_MATKEY_UVTRANSFORM(textureType, index), transforms); |
| if (result == aiReturn_SUCCESS) { |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("rotationUV"), |
| transforms.mRotation); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("positionU"), |
| transforms.mTranslation.x); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("positionV"), |
| transforms.mTranslation.y); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("scaleU"), |
| transforms.mScaling.x); |
| QSSGQmlUtilities::writeQmlPropertyHelper(output, |
| tabLevel + 1, |
| QSSGQmlUtilities::PropertyMap::Texture, |
| QStringLiteral("scaleV"), |
| transforms.mScaling.y); |
| } |
| // We don't make use of the data here, but there are additional flags |
| // available for example the usage of the alpha channel |
| // texture flags |
| //int textureFlags; |
| //material->Get(AI_MATKEY_TEXFLAGS(textureType, index), textureFlags); |
| |
| output << QSSGQmlUtilities::insertTabs(tabLevel) << QStringLiteral("}"); |
| |
| return outputString; |
| } |
| |
| void AssimpImporter::processAnimations(QTextStream &output) |
| { |
| for (int idx = 0; idx < m_animations.size(); ++idx) { |
| QHash<aiNode *, aiNodeAnim *> *animation = m_animations[idx]; |
| output << endl; |
| output << QSSGQmlUtilities::insertTabs(1) << "Timeline {" << endl; |
| output << QSSGQmlUtilities::insertTabs(2) << "id: timeline" << idx << endl; |
| output << QSSGQmlUtilities::insertTabs(2) << "startFrame: 0" << endl; |
| |
| QString keyframeString; |
| QTextStream keyframeStream(&keyframeString); |
| qreal endFrameTime = 0; |
| |
| for (auto itr = animation->begin(); itr != animation->end(); ++itr) { |
| aiNode *node = itr.key(); |
| |
| // We cannot set keyframes to nodes which do not have id. |
| if (!m_nodeIdMap.contains(node)) |
| continue; |
| QString id = m_nodeIdMap[node]; |
| |
| // We can set animation only on Node, Model, Camera or Light. |
| if (!m_nodeTypeMap.contains(node)) |
| continue; |
| QSSGQmlUtilities::PropertyMap::Type type = m_nodeTypeMap[node]; |
| if (type != QSSGQmlUtilities::PropertyMap::Node |
| && type != QSSGQmlUtilities::PropertyMap::Model |
| && type != QSSGQmlUtilities::PropertyMap::Camera |
| && type != QSSGQmlUtilities::PropertyMap::DirectionalLight |
| && type != QSSGQmlUtilities::PropertyMap::PointLight |
| && type != QSSGQmlUtilities::PropertyMap::AreaLight) |
| continue; |
| |
| aiNodeAnim *nodeAnim = itr.value(); |
| generateKeyframes(id, "position", nodeAnim->mNumPositionKeys, nodeAnim->mPositionKeys, |
| keyframeStream, endFrameTime); |
| generateKeyframes(id, "rotation", nodeAnim->mNumRotationKeys, nodeAnim->mRotationKeys, |
| keyframeStream, endFrameTime); |
| generateKeyframes(id, "scale", nodeAnim->mNumScalingKeys, nodeAnim->mScalingKeys, |
| keyframeStream, endFrameTime); |
| } |
| |
| output << QSSGQmlUtilities::insertTabs(2) << "endFrame: " << endFrameTime << endl; |
| output << QSSGQmlUtilities::insertTabs(2) << "currentFrame: 0" << endl; |
| // only the first set of animations is enabled for now. |
| output << QSSGQmlUtilities::insertTabs(2) << "enabled: " |
| << (animation == *m_animations.begin() ? "true" : "false") << endl; |
| output << QSSGQmlUtilities::insertTabs(2) << "animations: [" << endl; |
| output << QSSGQmlUtilities::insertTabs(3) << "TimelineAnimation {" << endl; |
| output << QSSGQmlUtilities::insertTabs(4) << "duration: " << endFrameTime << endl; |
| output << QSSGQmlUtilities::insertTabs(4) << "from: 0" << endl; |
| output << QSSGQmlUtilities::insertTabs(4) << "to: " << endFrameTime << endl; |
| output << QSSGQmlUtilities::insertTabs(4) << "running: true" << endl; |
| output << QSSGQmlUtilities::insertTabs(3) << "}" << endl; |
| output << QSSGQmlUtilities::insertTabs(2) << "]" << endl; |
| |
| output << keyframeString; |
| |
| output << QSSGQmlUtilities::insertTabs(1) << "}" << endl; |
| } |
| } |
| |
| namespace { |
| |
| QVector3D convertToQVector3D(const aiVector3D &vec) |
| { |
| return QVector3D(vec.x, vec.y, vec.z); |
| } |
| |
| QVector3D convertToQVector3D(const aiQuaternion &q) |
| { |
| return QQuaternion(q.w, q.x, q.y, q.z).toEulerAngles(); |
| } |
| |
| } |
| |
| template <typename T> |
| void AssimpImporter::generateKeyframes(const QString &id, const QString &propertyName, uint numKeys, const T *keys, |
| QTextStream &output, qreal &maxKeyframeTime) |
| { |
| output << endl; |
| output << QSSGQmlUtilities::insertTabs(2) << "KeyframeGroup {" << endl; |
| output << QSSGQmlUtilities::insertTabs(3) << "target: " << id << endl; |
| output << QSSGQmlUtilities::insertTabs(3) << "property: \"" << propertyName << "\"" << endl; |
| output << endl; |
| |
| struct Keyframe { |
| qreal time; |
| QVector3D value; |
| }; |
| |
| // First, convert all the keyframe values to QVector3D |
| // so that adjacent keyframes can be compared with qFuzzyCompare. |
| QList<Keyframe> keyframes; |
| for (uint i = 0; i < numKeys; ++i) { |
| T key = keys[i]; |
| Keyframe keyframe = {key.mTime, convertToQVector3D(key.mValue)}; |
| keyframes.push_back(keyframe); |
| if (i == numKeys-1) |
| maxKeyframeTime = qMax(maxKeyframeTime, keyframe.time); |
| } |
| |
| // Output all the Keyframes except similar ones. |
| for (int i = 0; i < keyframes.size(); ++i) { |
| const Keyframe &keyframe = keyframes[i]; |
| // Skip keyframes if those are very similar to adjacent ones. |
| if (i > 0 && i < keyframes.size()-1 |
| && qFuzzyCompare(keyframe.value, keyframes[i-1].value) |
| && qFuzzyCompare(keyframe.value, keyframes[i+1].value)) { |
| keyframes.removeAt(i--); |
| continue; |
| } |
| |
| output << QSSGQmlUtilities::insertTabs(3) << "Keyframe {" << endl; |
| output << QSSGQmlUtilities::insertTabs(4) << "frame: " << keyframe.time << endl; |
| output << QSSGQmlUtilities::insertTabs(4) << "value: " |
| << QSSGQmlUtilities::variantToQml(keyframe.value) << endl; |
| output << QSSGQmlUtilities::insertTabs(3) << "}" << endl; |
| } |
| output << QSSGQmlUtilities::insertTabs(2) << "}" << endl; |
| } |
| |
| bool AssimpImporter::isModel(aiNode *node) |
| { |
| return node && node->mNumMeshes > 0; |
| } |
| |
| bool AssimpImporter::isLight(aiNode *node) |
| { |
| return node && m_lights.contains(node); |
| } |
| |
| bool AssimpImporter::isCamera(aiNode *node) |
| { |
| return node && m_cameras.contains(node); |
| } |
| |
| QString AssimpImporter::generateUniqueId(const QString &id) |
| { |
| int index = 0; |
| QString uniqueID = id; |
| while (m_uniqueIds.contains(uniqueID)) |
| uniqueID = id + QStringLiteral("_") + QString::number(++index); |
| m_uniqueIds.insert(uniqueID); |
| return uniqueID; |
| } |
| |
| // This method is used to walk a subtree to see if any of the nodes actually |
| // add any state to the scene. A branch of empty transform nodes would only be |
| // useful if they were being used somewhere else (like where to aim a camera), |
| // but the general case is that they can be safely culled |
| bool AssimpImporter::containsNodesOfConsequence(aiNode *node) |
| { |
| bool isUseful = false; |
| |
| isUseful |= isLight(node); |
| isUseful |= isModel(node); |
| isUseful |= isCamera(node); |
| |
| // Return early if we know already |
| if (isUseful) |
| return true; |
| |
| for (uint i = 0; i < node->mNumChildren; ++i) |
| isUseful |= containsNodesOfConsequence(node->mChildren[i]); |
| |
| return isUseful; |
| } |
| |
| void AssimpImporter::processOptions(const QVariantMap &options) |
| { |
| // Setup import settings based given options |
| // You can either pass the whole options object, or just the "options" object |
| // so get the right scope. |
| QJsonObject optionsObject = QJsonObject::fromVariantMap(options); |
| if (optionsObject.contains(QStringLiteral("options"))) |
| optionsObject = optionsObject.value(QStringLiteral("options")).toObject(); |
| |
| if (optionsObject.isEmpty()) |
| return; |
| |
| // parse the options list for values |
| // We always need to triangulate and remove non triangles |
| m_postProcessSteps = aiPostProcessSteps(aiProcess_Triangulate | aiProcess_SortByPType); |
| |
| if (checkBooleanOption(QStringLiteral("calculateTangentSpace"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_CalcTangentSpace); |
| |
| if (checkBooleanOption(QStringLiteral("joinIdenticalVertices"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_JoinIdenticalVertices); |
| |
| if (checkBooleanOption(QStringLiteral("generateNormals"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_GenNormals); |
| |
| if (checkBooleanOption(QStringLiteral("generateSmoothNormals"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_GenSmoothNormals); |
| |
| if (checkBooleanOption(QStringLiteral("splitLargeMeshes"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_SplitLargeMeshes); |
| |
| if (checkBooleanOption(QStringLiteral("preTransformVertices"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_PreTransformVertices); |
| |
| if (checkBooleanOption(QStringLiteral("limitBoneWeights"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_LimitBoneWeights); |
| |
| if (checkBooleanOption(QStringLiteral("improveCacheLocality"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_ImproveCacheLocality); |
| |
| if (checkBooleanOption(QStringLiteral("removeRedundantMaterials"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_RemoveRedundantMaterials); |
| |
| if (checkBooleanOption(QStringLiteral("fixInfacingNormals"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_FixInfacingNormals); |
| |
| if (checkBooleanOption(QStringLiteral("findDegenerates"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_FindDegenerates); |
| |
| if (checkBooleanOption(QStringLiteral("findInvalidData"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_FindInvalidData); |
| |
| if (checkBooleanOption(QStringLiteral("transformUVCoordinates"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_TransformUVCoords); |
| |
| if (checkBooleanOption(QStringLiteral("findInstances"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_FindInstances); |
| |
| if (checkBooleanOption(QStringLiteral("optimizeMeshes"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_OptimizeMeshes); |
| |
| if (checkBooleanOption(QStringLiteral("optimizeGraph"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_OptimizeGraph); |
| |
| if (checkBooleanOption(QStringLiteral("globalScale"), optionsObject)) { |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_GlobalScale); |
| qreal globalScaleValue = getRealOption(QStringLiteral("globalScaleValue"), optionsObject); |
| if (globalScaleValue == 0.0) |
| globalScaleValue = 1.0; |
| m_importer->SetPropertyFloat(AI_CONFIG_GLOBAL_SCALE_FACTOR_KEY, ai_real(globalScaleValue)); |
| } |
| |
| if (checkBooleanOption(QStringLiteral("dropNormals"), optionsObject)) |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_DropNormals); |
| |
| aiComponent removeComponents = aiComponent(0); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentNormals"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_NORMALS); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentTangentsAndBitangents"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_TANGENTS_AND_BITANGENTS); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentColors"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_COLORS); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentUVs"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_TEXCOORDS); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentBoneWeights"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_BONEWEIGHTS); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentAnimations"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_ANIMATIONS); |
| |
| if (checkBooleanOption(QStringLiteral("removeComponentTextures"), optionsObject)) |
| removeComponents = aiComponent(removeComponents | aiComponent_TEXTURES); |
| |
| if (removeComponents != aiComponent(0)) { |
| m_postProcessSteps = aiPostProcessSteps(m_postProcessSteps | aiProcess_RemoveComponent); |
| m_importer->SetPropertyInteger(AI_CONFIG_PP_RVC_FLAGS, removeComponents); |
| } |
| } |
| |
| bool AssimpImporter::checkBooleanOption(const QString &optionName, const QJsonObject &options) |
| { |
| if (!options.contains(optionName)) |
| return false; |
| |
| QJsonObject option = options.value(optionName).toObject(); |
| return option.value(QStringLiteral("value")).toBool(); |
| } |
| |
| qreal AssimpImporter::getRealOption(const QString &optionName, const QJsonObject &options) |
| { |
| if (!options.contains(optionName)) |
| return false; |
| |
| QJsonObject option = options.value(optionName).toObject(); |
| return option.value(QStringLiteral("value")).toDouble(); |
| } |
| |
| QT_END_NAMESPACE |