Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "assimpimporter.h"
#include <assimputils.h>
#include <QtCore/qurl.h>
#include <QtCore/qbytearrayalgorithms.h>
#include <QtGui/QQuaternion>
#include <QtQml/QQmlFile>
#include <QtQuick3DAssetImport/private/qssgassetimporterfactory_p.h>
#include <QtQuick3DAssetImport/private/qssgassetimporter_p.h>
#include <QtQuick3DAssetUtils/private/qssgscenedesc_p.h>
#include <QtQuick3DAssetUtils/private/qssgsceneedit_p.h>
#include <QtQuick3DAssetUtils/private/qssgqmlutilities_p.h>
#include <QtQuick3DUtils/private/qssgutils_p.h>
// ASSIMP INC
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/Logger.hpp>
#include <assimp/DefaultLogger.hpp>
#include <assimp/postprocess.h>
#include <assimp/material.h>
#include <assimp/GltfMaterial.h>
#include <assimp/importerdesc.h>
#include <assimp/IOSystem.hpp>
#include <assimp/IOStream.hpp>
// ASSIMP INC
QT_BEGIN_NAMESPACE
//////////////////////// ASSIMP IMP
#define AI_GLTF_FILTER_NEAREST 0x2600
#define AI_GLTF_FILTER_LINEAR 0x2601
#define AI_GLTF_FILTER_NEAREST_MIPMAP_NEAREST 0x2700
#define AI_GLTF_FILTER_LINEAR_MIPMAP_NEAREST 0x2701
#define AI_GLTF_FILTER_NEAREST_MIPMAP_LINEAR 0x2702
#define AI_GLTF_FILTER_LINEAR_MIPMAP_LINEAR 0x2703
Q_REQUIRED_RESULT static inline QColor aiColorToQColor(const aiColor3D &color)
{
return QColor::fromRgbF(color.r, color.g, color.b, 1.0f);
}
Q_REQUIRED_RESULT static inline QColor aiColorToQColor(const aiColor4D &color)
{
return QColor::fromRgbF(color.r, color.g, color.b, color.a);
}
static QByteArray fromAiString(const aiString &string)
{
const qsizetype length = string.length;
return QByteArray(string.data, length);
}
struct NodeInfo
{
using Type = QSSGSceneDesc::Node::Type;
size_t index;
Type type;
};
Q_DECLARE_TYPEINFO(NodeInfo, Q_PRIMITIVE_TYPE);
using NodeMap = QHash<const aiNode *, NodeInfo>;
using AnimationNodeMap = QHash<QByteArray, QSSGSceneDesc::Node *>;
[[nodiscard]] static inline bool isEqual(const aiUVTransform &a, const aiUVTransform &b)
{
return (a.mTranslation == b.mTranslation && a.mScaling == b.mScaling && a.mRotation == b.mRotation);
};
struct TextureInfo
{
aiTextureMapMode modes[3] {};
aiTextureMapping mapping = aiTextureMapping::aiTextureMapping_UV;
unsigned int minFilter { AI_GLTF_FILTER_LINEAR };
unsigned int magFilter { AI_GLTF_FILTER_LINEAR };
uint uvIndex { 0 };
aiUVTransform transform;
};
bool operator==(const TextureInfo &a, const TextureInfo &b)
{
return (a.mapping == b.mapping)
&& (std::memcmp(a.modes, b.modes, sizeof(a.modes)) == 0)
&& (a.minFilter == b.minFilter)
&& (a.magFilter == b.magFilter)
&& (a.uvIndex == b.uvIndex)
&& isEqual(a.transform, b.transform);
}
struct TextureEntry
{
QByteArray name;
TextureInfo info;
QSSGSceneDesc::Texture *texture = nullptr;
};
size_t qHash(const TextureEntry &key, size_t seed)
{
static_assert(std::is_same_v<decltype(key.info.transform), aiUVTransform>, "Unexpected type");
const auto infoKey = quintptr(key.info.mapping)
^ (quintptr(key.info.modes[0]) ^ quintptr(key.info.modes[1]) ^ quintptr(key.info.modes[2]))
^ quintptr(key.info.minFilter ^ key.info.magFilter)
^ quintptr(key.info.uvIndex)
^ qHashBits(&key.info.transform, sizeof(aiUVTransform), seed);
return qHash(key.name, seed) ^ infoKey;
}
bool operator==(const TextureEntry &a, const TextureEntry &b)
{
return (a.name == b.name) && (a.info == b.info);
}
struct SceneInfo
{
struct Options
{
bool gltfMode = false;
bool fbxMode = false;
bool binaryKeyframes = false;
bool forceMipMapGeneration = false;
bool useFloatJointIndices = false;
bool designStudioWorkarounds = false;
float globalScaleValue = 1.0;
bool generateMeshLODs = false;
float lodNormalMergeAngle = 60.0;
float lodNormalSplitAngle = 25.0;
};
using MaterialMap = QVarLengthArray<QPair<const aiMaterial *, QSSGSceneDesc::Material *>>;
using MeshMap = QVarLengthArray<QPair<const aiMesh *, QSSGSceneDesc::Mesh *>>;
using EmbeddedTextureMap = QVarLengthArray<QSSGSceneDesc::TextureData *>;
using TextureMap = QSet<TextureEntry>;
struct skinData {
aiBone **mBones;
unsigned int mNumBones;
QSSGSceneDesc::Skin *node;
};
using SkinMap = QVarLengthArray<skinData>;
using Mesh2SkinMap = QVarLengthArray<qint16>;
const aiScene &scene;
MaterialMap &materialMap;
MeshMap &meshMap;
EmbeddedTextureMap &embeddedTextureMap;
TextureMap &textureMap;
SkinMap &skinMap;
Mesh2SkinMap &mesh2skin;
QDir workingDir;
Options opt;
};
class ResourceIOStream : public Assimp::IOStream
{
public:
ResourceIOStream(const char *pFile, const char *pMode);
// IOStream interface
size_t Read(void *pvBuffer, size_t pSize, size_t pCount) override;
size_t Write(const void *pvBuffer, size_t pSize, size_t pCount) override;
aiReturn Seek(size_t pOffset, aiOrigin pOrigin) override;
size_t Tell() const override;
size_t FileSize() const override;
void Flush() override;
private:
QFile file;
};
ResourceIOStream::ResourceIOStream(const char *pFile, const char *pMode) : file(QString::fromStdString(pFile))
{
QByteArray mode = QByteArray(pMode);
QFile::OpenMode openMode = QFile::NotOpen;
if (mode.startsWith("r"))
openMode |= QFile::ReadOnly;
else if (mode.startsWith("w"))
openMode |= QFile::WriteOnly;
if (mode.endsWith("t"))
openMode |= QFile::Text;
if (!file.open(openMode)) {
qWarning("Failed to open file %s: %s",
qPrintable(file.fileName()), qPrintable(file.errorString()));
}
}
size_t ResourceIOStream::Read(void *pvBuffer, size_t pSize, size_t pCount)
{
size_t ret = 0;
auto buffer = static_cast<char *>(pvBuffer);
for (ret = 0; ret < pCount; ret++) {
size_t read = file.read(buffer, pSize);
if (read != pSize)
return ret;
buffer += read;
}
return ret;
}
size_t ResourceIOStream::Write(const void *pvBuffer, size_t pSize, size_t pCount)
{
Q_UNUSED(pvBuffer);
Q_UNUSED(pSize);
Q_UNUSED(pCount);
Q_UNIMPLEMENTED();
return 0;
}
aiReturn ResourceIOStream::Seek(size_t pOffset, aiOrigin pOrigin)
{
switch (pOrigin) {
case aiOrigin_SET:
file.seek(pOffset);
break;
case aiOrigin_CUR:
file.seek(file.pos() + pOffset);
break;
case aiOrigin_END:
file.seek(file.size() + pOffset);
break;
default:
return aiReturn_FAILURE;
}
return aiReturn_SUCCESS;
}
size_t ResourceIOStream::Tell() const
{
return file.pos();
}
size_t ResourceIOStream::FileSize() const
{
return file.size();
}
void ResourceIOStream::Flush()
{
}
class ResourceIOSystem : public Assimp::IOSystem
{
public:
ResourceIOSystem();
// IOSystem interface
bool Exists(const char *pFile) const override;
char getOsSeparator() const override;
Assimp::IOStream *Open(const char *pFile, const char *pMode) override;
void Close(Assimp::IOStream *pFile) override;
};
ResourceIOSystem::ResourceIOSystem() : Assimp::IOSystem() { }
bool ResourceIOSystem::Exists(const char *pFile) const
{
return QFile::exists(QString::fromStdString(pFile));
}
char ResourceIOSystem::getOsSeparator() const
{
return QDir::separator().toLatin1();
}
Assimp::IOStream *ResourceIOSystem::Open(const char *pFile, const char *pMode)
{
return new ResourceIOStream(pFile, pMode);
}
void ResourceIOSystem::Close(Assimp::IOStream *pFile)
{
delete pFile;
}
static void setNodeProperties(QSSGSceneDesc::Node &target,
const aiNode &source,
const SceneInfo &sceneInfo,
aiMatrix4x4 *transformCorrection)
{
// objectName
if (target.name.isNull()) {
if (source.mName.length > 0)
target.name = fromAiString(source.mName);
else
target.name = QSSGQmlUtilities::getQmlElementName(target);
}
// Apply correction if necessary
aiMatrix4x4 transformMatrix;
if (transformCorrection)
transformMatrix = source.mTransformation * *transformCorrection;
else
transformMatrix = source.mTransformation;
// Decompose Transform Matrix to get properties
aiVector3D scaling;
aiQuaternion rotation;
aiVector3D translation;
transformMatrix.Decompose(scaling, rotation, translation);
// translate
if (!sceneInfo.opt.designStudioWorkarounds) {
QSSGSceneDesc::setProperty(target, "position", &QQuick3DNode::setPosition, QVector3D { translation.x, translation.y, translation.z });
} else {
QSSGSceneDesc::setProperty(target, "x", &QQuick3DNode::setX, translation.x);
QSSGSceneDesc::setProperty(target, "y", &QQuick3DNode::setY, translation.y);
QSSGSceneDesc::setProperty(target, "z", &QQuick3DNode::setZ, translation.z);
}
// rotation
const QQuaternion rot(rotation.w, rotation.x, rotation.y, rotation.z);
QSSGSceneDesc::setProperty(target, "rotation", &QQuick3DNode::setRotation, rot);
// scale
QSSGSceneDesc::setProperty(target, "scale", &QQuick3DNode::setScale, QVector3D { scaling.x, scaling.y, scaling.z });
// pivot
// opacity
// visible
}
static void setTextureProperties(QSSGSceneDesc::Texture &target, const TextureInfo &texInfo, const SceneInfo &sceneInfo)
{
const bool forceMipMapGeneration = sceneInfo.opt.forceMipMapGeneration;
if (texInfo.uvIndex > 0) {
// Quick3D supports 2 tex coords.
// According to gltf's khronos default implementation,
// the index will be selected to the nearest one.
QSSGSceneDesc::setProperty(target, "indexUV", &QQuick3DTexture::setIndexUV, 1);
}
// mapping
if (texInfo.mapping == aiTextureMapping_UV) {
// So we should be able to always hit this case by passing the right flags
// at import.
QSSGSceneDesc::setProperty(target, "mappingMode", &QQuick3DTexture::setMappingMode, QQuick3DTexture::MappingMode::UV);
// 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 (not supported)
static const auto asQtTilingMode = [](aiTextureMapMode mode) {
switch (mode) {
case aiTextureMapMode_Wrap:
return QQuick3DTexture::TilingMode::Repeat;
case aiTextureMapMode_Clamp:
return QQuick3DTexture::TilingMode::ClampToEdge;
case aiTextureMapMode_Mirror:
return QQuick3DTexture::TilingMode::MirroredRepeat;
default:
break;
}
return QQuick3DTexture::TilingMode::Repeat;
};
// mapping mode U
QSSGSceneDesc::setProperty(target, "tilingModeHorizontal", &QQuick3DTexture::setHorizontalTiling, asQtTilingMode(texInfo.modes[0]));
// mapping mode V
QSSGSceneDesc::setProperty(target, "tilingModeVertical", &QQuick3DTexture::setVerticalTiling, asQtTilingMode(texInfo.modes[1]));
const bool applyUvTransform = !isEqual(texInfo.transform, aiUVTransform());
if (applyUvTransform) {
// UV origins -
// glTF: 0, 1 (top left of texture)
// Assimp, Collada?, FBX?: 0.5, 0.5
// Quick3D: 0, 0 (bottom left of texture)
// Assimp already tries to fix it but it's not correct.
// So, we restore original values and then use pivot
const auto &transform = texInfo.transform;
float rotation = -transform.mRotation;
float rotationUV = qRadiansToDegrees(rotation);
float posU = transform.mTranslation.x;
float posV = transform.mTranslation.y;
if (sceneInfo.opt.gltfMode) {
const float rcos = std::cos(rotation);
const float rsin = std::sin(rotation);
posU -= 0.5f * transform.mScaling.x * (-rcos + rsin + 1.0f);
posV -= (0.5f * transform.mScaling.y * (rcos + rsin - 1.0f) + 1.0f - transform.mScaling.y);
QSSGSceneDesc::setProperty(target, "pivotV", &QQuick3DTexture::setPivotV, 1.0f);
} else {
QSSGSceneDesc::setProperty(target, "pivotU", &QQuick3DTexture::setPivotV, 0.5f);
QSSGSceneDesc::setProperty(target, "pivotV", &QQuick3DTexture::setPivotV, 0.5f);
}
QSSGSceneDesc::setProperty(target, "positionU", &QQuick3DTexture::setPositionU, posU);
QSSGSceneDesc::setProperty(target, "positionV", &QQuick3DTexture::setPositionV, posV);
QSSGSceneDesc::setProperty(target, "rotationUV", &QQuick3DTexture::setRotationUV, rotationUV);
QSSGSceneDesc::setProperty(target, "scaleU", &QQuick3DTexture::setScaleU, transform.mScaling.x);
QSSGSceneDesc::setProperty(target, "scaleV", &QQuick3DTexture::setScaleV, transform.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);
// Always generate and use mipmaps for imported assets
bool generateMipMaps = forceMipMapGeneration;
auto mipFilter = forceMipMapGeneration ? QQuick3DTexture::Filter::Linear : QQuick3DTexture::Filter::None;
// magFilter
auto filter = (texInfo.magFilter == AI_GLTF_FILTER_NEAREST) ? QQuick3DTexture::Filter::Nearest : QQuick3DTexture::Filter::Linear;
QSSGSceneDesc::setProperty(target, "magFilter", &QQuick3DTexture::setMagFilter, filter);
// minFilter
if (texInfo.minFilter == AI_GLTF_FILTER_NEAREST) {
filter = QQuick3DTexture::Filter::Nearest;
} else if (texInfo.minFilter == AI_GLTF_FILTER_LINEAR) {
filter = QQuick3DTexture::Filter::Linear;
} else if (texInfo.minFilter == AI_GLTF_FILTER_NEAREST_MIPMAP_NEAREST) {
filter = QQuick3DTexture::Filter::Nearest;
mipFilter = QQuick3DTexture::Filter::Nearest;
} else if (texInfo.minFilter == AI_GLTF_FILTER_LINEAR_MIPMAP_NEAREST) {
filter = QQuick3DTexture::Filter::Linear;
mipFilter = QQuick3DTexture::Filter::Nearest;
} else if (texInfo.minFilter == AI_GLTF_FILTER_NEAREST_MIPMAP_LINEAR) {
filter = QQuick3DTexture::Filter::Nearest;
mipFilter = QQuick3DTexture::Filter::Linear;
} else if (texInfo.minFilter == AI_GLTF_FILTER_LINEAR_MIPMAP_LINEAR) {
filter = QQuick3DTexture::Filter::Linear;
mipFilter = QQuick3DTexture::Filter::Linear;
}
QSSGSceneDesc::setProperty(target, "minFilter", &QQuick3DTexture::setMinFilter, filter);
// mipFilter
generateMipMaps = (mipFilter != QQuick3DTexture::Filter::None);
if (generateMipMaps) {
QSSGSceneDesc::setProperty(target, "generateMipmaps", &QQuick3DTexture::setGenerateMipmaps, true);
QSSGSceneDesc::setProperty(target, "mipFilter", &QQuick3DTexture::setMipFilter, mipFilter);
}
}
static void setMaterialProperties(QSSGSceneDesc::Material &target, const aiMaterial &source, const SceneInfo &sceneInfo, QSSGSceneDesc::Material::RuntimeType type)
{
if (target.name.isNull()) {
aiString materialName = source.GetName();
if (materialName.length > 0)
target.name = fromAiString(materialName);
else
target.name = QSSGQmlUtilities::getQmlElementName(target);
}
const auto createTextureNode = [&sceneInfo, &target](const aiMaterial &material, aiTextureType textureType, unsigned int index) {
const auto &srcScene = sceneInfo.scene;
QSSGSceneDesc::Texture *tex = nullptr;
aiString texturePath;
TextureInfo texInfo;
if (material.GetTexture(textureType, index, &texturePath, &texInfo.mapping, &texInfo.uvIndex, nullptr, nullptr, texInfo.modes) == aiReturn_SUCCESS) {
if (texturePath.length > 0) {
aiUVTransform transform;
if (material.Get(AI_MATKEY_UVTRANSFORM(textureType, index), transform) == aiReturn_SUCCESS)
texInfo.transform = transform;
material.Get(AI_MATKEY_UVWSRC(textureType, index), texInfo.uvIndex);
material.Get(AI_MATKEY_GLTF_MAPPINGFILTER_MIN(textureType, index), texInfo.minFilter);
material.Get(AI_MATKEY_GLTF_MAPPINGFILTER_MAG(textureType, index), texInfo.magFilter);
auto &textureMap = sceneInfo.textureMap;
QByteArray texName = QByteArray(texturePath.C_Str(), texturePath.length);
// Check if we already processed this texture
const auto it = textureMap.constFind(TextureEntry{texName, texInfo});
if (it != textureMap.cend()) {
Q_ASSERT(it->texture);
tex = it->texture;
} else {
// Two types, externally referenced or embedded
// Use the source file name as the identifier, since that will hopefully be fairly stable for re-import.
tex = new QSSGSceneDesc::Texture(QSSGSceneDesc::Texture::RuntimeType::Image2D, texName);
textureMap.insert(TextureEntry{fromAiString(texturePath), texInfo, tex});
QSSGSceneDesc::addNode(target, *tex);
setTextureProperties(*tex, texInfo, sceneInfo); // both
auto aEmbeddedTex = srcScene.GetEmbeddedTextureAndIndex(texturePath.C_Str());
const auto &embeddedTexId = aEmbeddedTex.second;
if (embeddedTexId > -1) {
QSSGSceneDesc::TextureData *textureData = nullptr;
auto &embeddedTextures = sceneInfo.embeddedTextureMap;
textureData = embeddedTextures[embeddedTexId];
if (!textureData) {
const auto *sourceTexture = aEmbeddedTex.first;
Q_ASSERT(sourceTexture->pcData);
// Two cases of embedded textures, uncompress and compressed.
const bool isCompressed = (sourceTexture->mHeight == 0);
// For compressed textures this is the size of the image buffer (in bytes)
const qsizetype asize = (isCompressed) ? sourceTexture->mWidth : (sourceTexture->mHeight * sourceTexture->mWidth) * sizeof(aiTexel);
const QSize size = (!isCompressed) ? QSize(int(sourceTexture->mWidth), int(sourceTexture->mHeight)) : QSize();
QByteArray imageData { reinterpret_cast<const char *>(sourceTexture->pcData), asize };
const auto format = (isCompressed) ? QByteArray(sourceTexture->achFormatHint) : QByteArrayLiteral("rgba8888");
const quint8 flags = isCompressed ? quint8(QSSGSceneDesc::TextureData::Flags::Compressed) : 0;
textureData = new QSSGSceneDesc::TextureData(imageData, size, format, flags);
QSSGSceneDesc::addNode(*tex, *textureData);
embeddedTextures[embeddedTexId] = textureData;
}
if (textureData)
QSSGSceneDesc::setProperty(*tex, "textureData", &QQuick3DTexture::setTextureData, textureData);
} else {
auto relativePath = QString::fromUtf8(texturePath.C_Str());
// Replace Windows separator to Unix separator
// so that assets including Windows relative path can be converted on Unix.
relativePath.replace("\\","/");
const auto path = sceneInfo.workingDir.absoluteFilePath(relativePath);
QSSGSceneDesc::setProperty(*tex, "source", &QQuick3DTexture::setSource, QUrl{ path });
}
}
}
}
return tex;
};
aiReturn result;
if (type == QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial) {
{
aiColor4D baseColorFactor;
result = source.Get(AI_MATKEY_BASE_COLOR, baseColorFactor);
if (result == aiReturn_SUCCESS) {
// Some special handling required since baseColorFactor's are stored as linear factors and need to be converted for Qt Quick
const QColor sRGBBaseColorFactor = QSSGUtils::color::linearTosRGB(QVector4D(baseColorFactor.r, baseColorFactor.g, baseColorFactor.b, baseColorFactor.a));
QSSGSceneDesc::setProperty(target, "baseColor", &QQuick3DPrincipledMaterial::setBaseColor, sRGBBaseColorFactor);
} else {
// Also try diffuse color as a fallback
aiColor3D diffuseColor;
result = source.Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "baseColor", &QQuick3DPrincipledMaterial::setBaseColor, aiColorToQColor(diffuseColor));
}
}
if (auto baseColorTexture = createTextureNode(source, AI_MATKEY_BASE_COLOR_TEXTURE)) {
QSSGSceneDesc::setProperty(target, "baseColorMap", &QQuick3DPrincipledMaterial::setBaseColorMap, baseColorTexture);
QSSGSceneDesc::setProperty(target, "opacityChannel", &QQuick3DPrincipledMaterial::setOpacityChannel, QQuick3DPrincipledMaterial::TextureChannelMapping::A);
} else if (auto diffuseMapTexture = createTextureNode(source, aiTextureType_DIFFUSE, 0)) {
// Also try to legacy diffuse texture as an alternative
QSSGSceneDesc::setProperty(target, "baseColorMap", &QQuick3DPrincipledMaterial::setBaseColorMap, diffuseMapTexture);
}
if (auto metalicRoughnessTexture = createTextureNode(source, AI_MATKEY_GLTF_PBRMETALLICROUGHNESS_METALLICROUGHNESS_TEXTURE)) {
QSSGSceneDesc::setProperty(target, "metalnessMap", &QQuick3DPrincipledMaterial::setMetalnessMap, metalicRoughnessTexture);
QSSGSceneDesc::setProperty(target, "metalnessChannel", &QQuick3DPrincipledMaterial::setMetalnessChannel, QQuick3DPrincipledMaterial::TextureChannelMapping::B);
QSSGSceneDesc::setProperty(target, "roughnessMap", &QQuick3DPrincipledMaterial::setRoughnessMap, metalicRoughnessTexture);
QSSGSceneDesc::setProperty(target, "roughnessChannel", &QQuick3DPrincipledMaterial::setRoughnessChannel, QQuick3DPrincipledMaterial::TextureChannelMapping::G);
}
{
ai_real metallicFactor;
result = source.Get(AI_MATKEY_METALLIC_FACTOR, metallicFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "metalness", &QQuick3DPrincipledMaterial::setMetalness, float(metallicFactor));
}
{
ai_real roughnessFactor;
result = source.Get(AI_MATKEY_ROUGHNESS_FACTOR, roughnessFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "roughness", &QQuick3DPrincipledMaterial::setRoughness, float(roughnessFactor));
}
if (auto normalTexture = createTextureNode(source, aiTextureType_NORMALS, 0)) {
QSSGSceneDesc::setProperty(target, "normalMap", &QQuick3DPrincipledMaterial::setNormalMap, normalTexture);
{
ai_real normalScale;
result = source.Get(AI_MATKEY_GLTF_TEXTURE_SCALE(aiTextureType_NORMALS, 0), normalScale);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "normalStrength", &QQuick3DPrincipledMaterial::setNormalStrength, float(normalScale));
}
}
// Occlusion Textures are not implimented (yet)
if (auto occlusionTexture = createTextureNode(source, aiTextureType_LIGHTMAP, 0)) {
QSSGSceneDesc::setProperty(target, "occlusionMap", &QQuick3DPrincipledMaterial::setOcclusionMap, occlusionTexture);
QSSGSceneDesc::setProperty(target, "occlusionChannel", &QQuick3DPrincipledMaterial::setOcclusionChannel, QQuick3DPrincipledMaterial::TextureChannelMapping::R);
{
ai_real occlusionAmount;
result = source.Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(aiTextureType_LIGHTMAP, 0), occlusionAmount);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "occlusionAmount", &QQuick3DPrincipledMaterial::setOcclusionAmount, float(occlusionAmount));
}
}
if (auto emissiveTexture = createTextureNode(source, aiTextureType_EMISSIVE, 0))
QSSGSceneDesc::setProperty(target, "emissiveMap", &QQuick3DPrincipledMaterial::setEmissiveMap, emissiveTexture);
{
aiColor3D emissiveColorFactor;
result = source.Get(AI_MATKEY_COLOR_EMISSIVE, emissiveColorFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "emissiveFactor", &QQuick3DPrincipledMaterial::setEmissiveFactor, QVector3D { emissiveColorFactor.r, emissiveColorFactor.g, emissiveColorFactor.b });
}
{
bool isDoubleSided;
result = source.Get(AI_MATKEY_TWOSIDED, isDoubleSided);
if (result == aiReturn_SUCCESS && isDoubleSided)
QSSGSceneDesc::setProperty(target, "cullMode", &QQuick3DPrincipledMaterial::setCullMode, QQuick3DPrincipledMaterial::CullMode::NoCulling);
}
{
aiString alphaMode;
result = source.Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode);
if (result == aiReturn_SUCCESS) {
auto mode = QQuick3DPrincipledMaterial::AlphaMode::Default;
if (QByteArrayView(alphaMode.C_Str()) == "OPAQUE")
mode = QQuick3DPrincipledMaterial::AlphaMode::Opaque;
else if (QByteArrayView(alphaMode.C_Str()) == "MASK")
mode = QQuick3DPrincipledMaterial::AlphaMode::Mask;
else if (QByteArrayView(alphaMode.C_Str()) == "BLEND")
mode = QQuick3DPrincipledMaterial::AlphaMode::Blend;
if (mode != QQuick3DPrincipledMaterial::AlphaMode::Default) {
QSSGSceneDesc::setProperty(target, "alphaMode", &QQuick3DPrincipledMaterial::setAlphaMode, mode);
// If the mode is mask, we also need to force OpaquePrePassDepthDraw mode
if (mode == QQuick3DPrincipledMaterial::AlphaMode::Mask)
QSSGSceneDesc::setProperty(target, "depthDrawMode", &QQuick3DPrincipledMaterial::setDepthDrawMode, QQuick3DMaterial::OpaquePrePassDepthDraw);
}
}
}
{
ai_real alphaCutoff;
result = source.Get(AI_MATKEY_GLTF_ALPHACUTOFF, alphaCutoff);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "alphaCutoff", &QQuick3DPrincipledMaterial::setAlphaCutoff, float(alphaCutoff));
}
{
int shadingModel = 0;
result = source.Get(AI_MATKEY_SHADING_MODEL, shadingModel);
if (result == aiReturn_SUCCESS && shadingModel == aiShadingMode_Unlit)
QSSGSceneDesc::setProperty(target, "lighting", &QQuick3DPrincipledMaterial::setLighting, QQuick3DPrincipledMaterial::Lighting::NoLighting);
}
{
// Clearcoat Properties (KHR_materials_clearcoat)
// factor
{
ai_real clearcoatFactor = 0.0f;
result = source.Get(AI_MATKEY_CLEARCOAT_FACTOR, clearcoatFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"clearcoatAmount",
&QQuick3DPrincipledMaterial::setClearcoatAmount,
float(clearcoatFactor));
}
// roughness
{
ai_real clearcoatRoughnessFactor = 0.0f;
result = source.Get(AI_MATKEY_CLEARCOAT_ROUGHNESS_FACTOR, clearcoatRoughnessFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"clearcoatRoughnessAmount",
&QQuick3DPrincipledMaterial::setClearcoatRoughnessAmount,
float(clearcoatRoughnessFactor));
}
// texture
if (auto clearcoatTexture = createTextureNode(source, AI_MATKEY_CLEARCOAT_TEXTURE))
QSSGSceneDesc::setProperty(target, "clearcoatMap", &QQuick3DPrincipledMaterial::setClearcoatMap, clearcoatTexture);
// roughness texture
if (auto clearcoatRoughnessTexture = createTextureNode(source, AI_MATKEY_CLEARCOAT_ROUGHNESS_TEXTURE))
QSSGSceneDesc::setProperty(target,
"clearcoatRoughnessMap",
&QQuick3DPrincipledMaterial::setClearcoatRoughnessMap,
clearcoatRoughnessTexture);
// normal texture
if (auto clearcoatNormalTexture = createTextureNode(source, AI_MATKEY_CLEARCOAT_NORMAL_TEXTURE))
QSSGSceneDesc::setProperty(target, "clearcoatNormalMap", &QQuick3DPrincipledMaterial::setClearcoatNormalMap, clearcoatNormalTexture);
}
{
// Transmission Properties (KHR_materials_transmission)
// factor
{
ai_real transmissionFactor = 0.0f;
result = source.Get(AI_MATKEY_TRANSMISSION_FACTOR, transmissionFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"transmissionFactor",
&QQuick3DPrincipledMaterial::setTransmissionFactor,
float(transmissionFactor));
}
// texture
{
if (auto transmissionImage = createTextureNode(source, AI_MATKEY_TRANSMISSION_TEXTURE))
QSSGSceneDesc::setProperty(target,
"transmissionMap",
&QQuick3DPrincipledMaterial::setTransmissionMap,
transmissionImage);
}
}
{
// Volume Properties (KHR_materials_volume) [only used with transmission]
// thicknessFactor
{
ai_real thicknessFactor = 0.0f;
result = source.Get(AI_MATKEY_VOLUME_THICKNESS_FACTOR, thicknessFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "thicknessFactor", &QQuick3DPrincipledMaterial::setThicknessFactor, float(thicknessFactor));
}
// thicknessMap
{
if (auto thicknessImage = createTextureNode(source, AI_MATKEY_VOLUME_THICKNESS_TEXTURE))
QSSGSceneDesc::setProperty(target, "thicknessMap", &QQuick3DPrincipledMaterial::setThicknessMap, thicknessImage);
}
// attenuationDistance
{
ai_real attenuationDistance = 0.0f;
result = source.Get(AI_MATKEY_VOLUME_ATTENUATION_DISTANCE, attenuationDistance);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"attenuationDistance",
&QQuick3DPrincipledMaterial::setAttenuationDistance,
float(attenuationDistance));
}
// attenuationColor
{
aiColor3D attenuationColor;
result = source.Get(AI_MATKEY_VOLUME_ATTENUATION_COLOR, attenuationColor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"attenuationColor",
&QQuick3DPrincipledMaterial::setAttenuationColor,
aiColorToQColor(attenuationColor));
}
}
// KHR_materials_ior
{
ai_real ior = 0.0f;
result = source.Get(AI_MATKEY_REFRACTI, ior);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"indexOfRefraction",
&QQuick3DPrincipledMaterial::setIndexOfRefraction,
float(ior));
}
{
// opacity AI_MATKEY_OPACITY
ai_real opacity = 1.0f;
result = source.Get(AI_MATKEY_OPACITY, opacity);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "opacity", &QQuick3DPrincipledMaterial::setOpacity, float(opacity));
// opacityMap aiTextureType_OPACITY 0
if (auto opacityTexture = createTextureNode(source, aiTextureType_OPACITY, 0))
QSSGSceneDesc::setProperty(target, "opacityMap", &QQuick3DPrincipledMaterial::setOpacityMap, opacityTexture);
}
} else if (type == QSSGSceneDesc::Material::RuntimeType::DefaultMaterial) { // Ver1
int shadingModel = 0;
auto material = &source;
result = material->Get(AI_MATKEY_SHADING_MODEL, shadingModel);
// lighting
if (result == aiReturn_SUCCESS && (shadingModel == aiShadingMode_NoShading))
QSSGSceneDesc::setProperty(target, "lighting", &QQuick3DDefaultMaterial::setLighting, QQuick3DDefaultMaterial::Lighting::NoLighting);
if (auto diffuseMapTexture = createTextureNode(source, aiTextureType_DIFFUSE, 0)) {
QSSGSceneDesc::setProperty(target, "diffuseMap", &QQuick3DDefaultMaterial::setDiffuseMap, diffuseMapTexture);
} else {
// 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:
aiColor3D diffuseColor;
result = material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuseColor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "diffuseColor", &QQuick3DDefaultMaterial::setDiffuseColor, aiColorToQColor(diffuseColor));
}
if (auto emissiveTexture = createTextureNode(source, aiTextureType_EMISSIVE, 0))
QSSGSceneDesc::setProperty(target, "emissiveMap", &QQuick3DDefaultMaterial::setEmissiveMap, emissiveTexture);
// specularReflectionMap
if (auto specularTexture = createTextureNode(source, aiTextureType_SPECULAR, 0))
QSSGSceneDesc::setProperty(target, "specularMap", &QQuick3DDefaultMaterial::setSpecularMap, specularTexture);
// opacity AI_MATKEY_OPACITY
ai_real opacity;
result = material->Get(AI_MATKEY_OPACITY, opacity);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "opacity", &QQuick3DDefaultMaterial::setOpacity, float(opacity));
// opacityMap aiTextureType_OPACITY 0
if (auto opacityTexture = createTextureNode(source, aiTextureType_OPACITY, 0))
QSSGSceneDesc::setProperty(target, "opacityMap", &QQuick3DDefaultMaterial::setOpacityMap, opacityTexture);
// bumpMap aiTextureType_HEIGHT 0
if (auto bumpTexture = createTextureNode(source, aiTextureType_HEIGHT, 0)) {
QSSGSceneDesc::setProperty(target, "bumpMap", &QQuick3DDefaultMaterial::setBumpMap, bumpTexture);
// bumpAmount AI_MATKEY_BUMPSCALING
ai_real bumpAmount;
result = material->Get(AI_MATKEY_BUMPSCALING, bumpAmount);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "bumpAmount", &QQuick3DDefaultMaterial::setBumpAmount, float(bumpAmount));
}
// normalMap aiTextureType_NORMALS 0
if (auto normalTexture = createTextureNode(source, aiTextureType_NORMALS, 0))
QSSGSceneDesc::setProperty(target, "normalMap", &QQuick3DDefaultMaterial::setNormalMap, normalTexture);
} else if (type == QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial) {
{
aiColor4D albedoFactor;
result = source.Get(AI_MATKEY_COLOR_DIFFUSE, albedoFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "albedoColor", &QQuick3DSpecularGlossyMaterial::setAlbedoColor, aiColorToQColor(albedoFactor));
}
if (auto albedoTexture = createTextureNode(source, aiTextureType_DIFFUSE, 0)) {
QSSGSceneDesc::setProperty(target, "albedoMap", &QQuick3DSpecularGlossyMaterial::setAlbedoMap, albedoTexture);
QSSGSceneDesc::setProperty(target, "opacityChannel", &QQuick3DSpecularGlossyMaterial::setOpacityChannel, QQuick3DSpecularGlossyMaterial::TextureChannelMapping::A);
}
if (auto specularGlossinessTexture = createTextureNode(source, aiTextureType_SPECULAR, 0)) {
QSSGSceneDesc::setProperty(target, "specularMap", &QQuick3DSpecularGlossyMaterial::setSpecularMap, specularGlossinessTexture);
QSSGSceneDesc::setProperty(target, "glossinessMap", &QQuick3DSpecularGlossyMaterial::setGlossinessMap, specularGlossinessTexture);
QSSGSceneDesc::setProperty(target, "glossinessChannel", &QQuick3DSpecularGlossyMaterial::setGlossinessChannel, QQuick3DSpecularGlossyMaterial::TextureChannelMapping::A);
}
{
aiColor4D specularColorFactor;
result = source.Get(AI_MATKEY_COLOR_SPECULAR, specularColorFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "specularColor", &QQuick3DSpecularGlossyMaterial::setSpecularColor, aiColorToQColor(specularColorFactor));
}
{
ai_real glossinessFactor;
result = source.Get(AI_MATKEY_GLOSSINESS_FACTOR, glossinessFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "glossiness", &QQuick3DSpecularGlossyMaterial::setGlossiness, float(glossinessFactor));
}
if (auto normalTexture = createTextureNode(source, aiTextureType_NORMALS, 0)) {
QSSGSceneDesc::setProperty(target, "normalMap", &QQuick3DSpecularGlossyMaterial::setNormalMap, normalTexture);
{
ai_real normalScale;
result = source.Get(AI_MATKEY_GLTF_TEXTURE_SCALE(aiTextureType_NORMALS, 0), normalScale);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "normalStrength", &QQuick3DSpecularGlossyMaterial::setNormalStrength, float(normalScale));
}
}
// Occlusion Textures are not implimented (yet)
if (auto occlusionTexture = createTextureNode(source, aiTextureType_LIGHTMAP, 0)) {
QSSGSceneDesc::setProperty(target, "occlusionMap", &QQuick3DSpecularGlossyMaterial::setOcclusionMap, occlusionTexture);
QSSGSceneDesc::setProperty(target, "occlusionChannel", &QQuick3DSpecularGlossyMaterial::setOcclusionChannel, QQuick3DSpecularGlossyMaterial::TextureChannelMapping::R);
{
ai_real occlusionAmount;
result = source.Get(AI_MATKEY_GLTF_TEXTURE_STRENGTH(aiTextureType_LIGHTMAP, 0), occlusionAmount);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "occlusionAmount", &QQuick3DSpecularGlossyMaterial::setOcclusionAmount, float(occlusionAmount));
}
}
if (auto emissiveTexture = createTextureNode(source, aiTextureType_EMISSIVE, 0))
QSSGSceneDesc::setProperty(target, "emissiveMap", &QQuick3DSpecularGlossyMaterial::setEmissiveMap, emissiveTexture);
{
aiColor3D emissiveColorFactor;
result = source.Get(AI_MATKEY_COLOR_EMISSIVE, emissiveColorFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "emissiveFactor", &QQuick3DSpecularGlossyMaterial::setEmissiveFactor, QVector3D { emissiveColorFactor.r, emissiveColorFactor.g, emissiveColorFactor.b });
}
{
bool isDoubleSided;
result = source.Get(AI_MATKEY_TWOSIDED, isDoubleSided);
if (result == aiReturn_SUCCESS && isDoubleSided)
QSSGSceneDesc::setProperty(target, "cullMode", &QQuick3DSpecularGlossyMaterial::setCullMode, QQuick3DSpecularGlossyMaterial::CullMode::NoCulling);
}
{
aiString alphaMode;
result = source.Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode);
if (result == aiReturn_SUCCESS) {
auto mode = QQuick3DSpecularGlossyMaterial::AlphaMode::Default;
if (QByteArrayView(alphaMode.C_Str()) == "OPAQUE")
mode = QQuick3DSpecularGlossyMaterial::AlphaMode::Opaque;
else if (QByteArrayView(alphaMode.C_Str()) == "MASK")
mode = QQuick3DSpecularGlossyMaterial::AlphaMode::Mask;
else if (QByteArrayView(alphaMode.C_Str()) == "BLEND")
mode = QQuick3DSpecularGlossyMaterial::AlphaMode::Blend;
if (mode != QQuick3DSpecularGlossyMaterial::AlphaMode::Default) {
QSSGSceneDesc::setProperty(target, "alphaMode", &QQuick3DSpecularGlossyMaterial::setAlphaMode, mode);
// If the mode is mask, we also need to force OpaquePrePassDepthDraw mode
if (mode == QQuick3DSpecularGlossyMaterial::AlphaMode::Mask)
QSSGSceneDesc::setProperty(target, "depthDrawMode", &QQuick3DSpecularGlossyMaterial::setDepthDrawMode, QQuick3DMaterial::OpaquePrePassDepthDraw);
}
}
}
{
ai_real alphaCutoff;
result = source.Get(AI_MATKEY_GLTF_ALPHACUTOFF, alphaCutoff);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "alphaCutoff", &QQuick3DSpecularGlossyMaterial::setAlphaCutoff, float(alphaCutoff));
}
{
int shadingModel = 0;
result = source.Get(AI_MATKEY_SHADING_MODEL, shadingModel);
if (result == aiReturn_SUCCESS && shadingModel == aiShadingMode_Unlit)
QSSGSceneDesc::setProperty(target, "lighting", &QQuick3DSpecularGlossyMaterial::setLighting, QQuick3DSpecularGlossyMaterial::Lighting::NoLighting);
}
{
// Clearcoat Properties (KHR_materials_clearcoat)
// factor
{
ai_real clearcoatFactor = 0.0f;
result = source.Get(AI_MATKEY_CLEARCOAT_FACTOR, clearcoatFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"clearcoatAmount",
&QQuick3DSpecularGlossyMaterial::setClearcoatAmount,
float(clearcoatFactor));
}
// roughness
{
ai_real clearcoatRoughnessFactor = 0.0f;
result = source.Get(AI_MATKEY_CLEARCOAT_ROUGHNESS_FACTOR, clearcoatRoughnessFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"clearcoatRoughnessAmount",
&QQuick3DSpecularGlossyMaterial::setClearcoatRoughnessAmount,
float(clearcoatRoughnessFactor));
}
// texture
if (auto clearcoatTexture = createTextureNode(source, AI_MATKEY_CLEARCOAT_TEXTURE))
QSSGSceneDesc::setProperty(target, "clearcoatMap", &QQuick3DSpecularGlossyMaterial::setClearcoatMap, clearcoatTexture);
// roughness texture
if (auto clearcoatRoughnessTexture = createTextureNode(source, AI_MATKEY_CLEARCOAT_ROUGHNESS_TEXTURE))
QSSGSceneDesc::setProperty(target,
"clearcoatRoughnessMap",
&QQuick3DSpecularGlossyMaterial::setClearcoatRoughnessMap,
clearcoatRoughnessTexture);
// normal texture
if (auto clearcoatNormalTexture = createTextureNode(source, AI_MATKEY_CLEARCOAT_NORMAL_TEXTURE)) {
QSSGSceneDesc::setProperty(target, "clearcoatNormalMap", &QQuick3DSpecularGlossyMaterial::setClearcoatNormalMap, clearcoatNormalTexture);
ai_real clearcoatNormalStrength = 0.0f;
result = source.Get(AI_MATKEY_GLTF_TEXTURE_SCALE(aiTextureType_CLEARCOAT, 2), clearcoatNormalTexture);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "clearcoatNormalStrength", &QQuick3DPrincipledMaterial::setClearcoatNormalStrength, float(clearcoatNormalStrength));
}
}
{
// Transmission Properties (KHR_materials_transmission)
// factor
{
ai_real transmissionFactor = 0.0f;
result = source.Get(AI_MATKEY_TRANSMISSION_FACTOR, transmissionFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"transmissionFactor",
&QQuick3DSpecularGlossyMaterial::setTransmissionFactor,
float(transmissionFactor));
}
// texture
{
if (auto transmissionImage = createTextureNode(source, AI_MATKEY_TRANSMISSION_TEXTURE))
QSSGSceneDesc::setProperty(target,
"transmissionMap",
&QQuick3DSpecularGlossyMaterial::setTransmissionMap,
transmissionImage);
}
}
{
// Volume Properties (KHR_materials_volume) [only used with transmission]
// thicknessFactor
{
ai_real thicknessFactor = 0.0f;
result = source.Get(AI_MATKEY_VOLUME_THICKNESS_FACTOR, thicknessFactor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target, "thicknessFactor", &QQuick3DSpecularGlossyMaterial::setThicknessFactor, float(thicknessFactor));
}
// thicknessMap
{
if (auto thicknessImage = createTextureNode(source, AI_MATKEY_VOLUME_THICKNESS_TEXTURE))
QSSGSceneDesc::setProperty(target, "thicknessMap", &QQuick3DSpecularGlossyMaterial::setThicknessMap, thicknessImage);
}
// attenuationDistance
{
ai_real attenuationDistance = 0.0f;
result = source.Get(AI_MATKEY_VOLUME_ATTENUATION_DISTANCE, attenuationDistance);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"attenuationDistance",
&QQuick3DSpecularGlossyMaterial::setAttenuationDistance,
float(attenuationDistance));
}
// attenuationColor
{
aiColor3D attenuationColor;
result = source.Get(AI_MATKEY_VOLUME_ATTENUATION_COLOR, attenuationColor);
if (result == aiReturn_SUCCESS)
QSSGSceneDesc::setProperty(target,
"attenuationColor",
&QQuick3DSpecularGlossyMaterial::setAttenuationColor,
aiColorToQColor(attenuationColor));
}
}
}
}
static void setCameraProperties(QSSGSceneDesc::Camera &target, const aiCamera &source, const aiNode &sourceNode, const SceneInfo &sceneInfo)
{
using namespace QSSGSceneDesc;
// assimp does not have a camera type but it works for gltf2 format.
target.runtimeType = (source.mHorizontalFOV == 0.0f) ? Node::RuntimeType::OrthographicCamera
: Node::RuntimeType::PerspectiveCamera;
// We assume these default forward and up vectors, so if this isn't
// the case we have to do additional transform
aiMatrix4x4 correctionMatrix;
bool needsCorrection = false;
// Workaround For FBX,
// assimp has a problem to set properties, mLookAt ans mUp
// and it takes too much time for correction.
// Quick3D will ignore these value and just use
// the initial differences between FBX and Quick3D.
if (sceneInfo.opt.fbxMode) {
aiMatrix4x4::RotationY(ai_real(M_PI / 2), correctionMatrix);
needsCorrection = true;
} else {
aiVector3D upQuick3D = aiVector3D(0, 1, 0);
if (source.mLookAt != aiVector3D(0, 0, -1)) {
aiMatrix4x4 lookAtCorrection;
aiMatrix4x4::FromToMatrix(aiVector3D(0, 0, -1), source.mLookAt, lookAtCorrection);
correctionMatrix *= lookAtCorrection;
needsCorrection = true;
upQuick3D *= lookAtCorrection;
}
if (source.mUp != upQuick3D) {
aiMatrix4x4 upCorrection;
aiMatrix4x4::FromToMatrix(upQuick3D, source.mUp, upCorrection);
correctionMatrix = upCorrection * correctionMatrix;
needsCorrection = true;
}
}
setNodeProperties(target, sourceNode, sceneInfo, needsCorrection ? &correctionMatrix : nullptr);
// clipNear and clipFar
if (target.runtimeType == Node::RuntimeType::PerspectiveCamera) {
setProperty(target, "clipNear", &QQuick3DPerspectiveCamera::setClipNear, source.mClipPlaneNear);
setProperty(target, "clipFar", &QQuick3DPerspectiveCamera::setClipFar, source.mClipPlaneFar);
} else { //OrthographicCamera
setProperty(target, "clipNear", &QQuick3DOrthographicCamera::setClipNear, source.mClipPlaneNear);
setProperty(target, "clipFar", &QQuick3DOrthographicCamera::setClipFar, source.mClipPlaneFar);
}
if (target.runtimeType == Node::RuntimeType::PerspectiveCamera) {
// fieldOfView
// mHorizontalFOV is defined as a half horizontal fov
// in the assimp header but it seems not half now.
const float fov = qRadiansToDegrees(source.mHorizontalFOV);
setProperty(target, "fieldOfView", &QQuick3DPerspectiveCamera::setFieldOfView, fov);
// isFieldOfViewHorizontal
setProperty(target, "fieldOfViewOrientation", &QQuick3DPerspectiveCamera::setFieldOfViewOrientation,
QQuick3DPerspectiveCamera::FieldOfViewOrientation::Horizontal);
} else { //OrthographicCamera
const float width = source.mOrthographicWidth * 2.0f;
const float height = width / source.mAspect;
setProperty(target, "horizontalMagnification", &QQuick3DOrthographicCamera::setHorizontalMagnification, width);
setProperty(target, "verticalMagnification", &QQuick3DOrthographicCamera::setVerticalMagnification, height);
}
// projectionMode
// scaleMode
// scaleAnchor
// frustomScaleX
// frustomScaleY
}
static void setLightProperties(QSSGSceneDesc::Light &target, const aiLight &source, const aiNode &sourceNode, const SceneInfo &sceneInfo)
{
// 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;
bool needsCorrection = false;
if (source.mDirection != aiVector3D(0, 0, 0)) {
if (source.mDirection != aiVector3D(0, 0, -1)) {
aiMatrix4x4::FromToMatrix(aiVector3D(0, 0, -1), source.mDirection, correctionMatrix);
needsCorrection = true;
}
}
// lightType
static const auto asQtLightType = [](aiLightSourceType type) {
switch (type) {
case aiLightSource_AMBIENT:
Q_FALLTHROUGH();
case aiLightSource_DIRECTIONAL:
return QSSGSceneDesc::Light::RuntimeType::DirectionalLight;
case aiLightSource_POINT:
return QSSGSceneDesc::Light::RuntimeType::PointLight;
case aiLightSource_SPOT:
return QSSGSceneDesc::Light::RuntimeType::SpotLight;
default:
return QSSGSceneDesc::Light::RuntimeType::PointLight;
}
};
target.runtimeType = asQtLightType(source.mType);
setNodeProperties(target, sourceNode, sceneInfo, needsCorrection ? &correctionMatrix : nullptr);
// brightness
// Assimp has no property related to brightness or intensity.
// They are multiplied to diffuse, ambient and specular colors.
// For extracting the property value, we will check the maximum value of them.
// (In most cases, Assimp uses the same specular values with diffuse values,
// so we will compare just components of the diffuse and the ambient)
float brightness = qMax(qMax(1.0f, source.mColorDiffuse.r),
qMax(source.mColorDiffuse.g, source.mColorDiffuse.b));
// ambientColor
if (source.mType == aiLightSource_AMBIENT) {
brightness = qMax(qMax(brightness, source.mColorAmbient.r),
qMax(source.mColorAmbient.g, source.mColorAmbient.b));
// We only want ambient light color if it is explicit
const QColor ambientColor = QColor::fromRgbF(source.mColorAmbient.r / brightness,
source.mColorAmbient.g / brightness,
source.mColorAmbient.b / brightness);
QSSGSceneDesc::setProperty(target, "ambientColor", &QQuick3DAbstractLight::setAmbientColor, ambientColor);
}
// diffuseColor
const QColor diffuseColor = QColor::fromRgbF(source.mColorDiffuse.r / brightness,
source.mColorDiffuse.g / brightness,
source.mColorDiffuse.b / brightness);
QSSGSceneDesc::setProperty(target, "color", &QQuick3DAbstractLight::setColor, diffuseColor);
// describe brightness here
QSSGSceneDesc::setProperty(target, "brightness", &QQuick3DAbstractLight::setBrightness, brightness);
const bool isSpot = (source.mType == aiLightSource_SPOT);
if (source.mType == aiLightSource_POINT || isSpot) {
// constantFade
// Some assets have this constant attenuation value as 0.0f and it makes light attenuation makes infinite at distance 0.
// In that case, we will use the default constant attenuation, 1.0f.
const bool hasAttConstant = !qFuzzyIsNull(source.mAttenuationConstant);
if (isSpot) {
if (hasAttConstant)
QSSGSceneDesc::setProperty(target, "constantFade", &QQuick3DSpotLight::setConstantFade, source.mAttenuationConstant);
QSSGSceneDesc::setProperty(target, "linearFade", &QQuick3DSpotLight::setLinearFade, source.mAttenuationLinear * 100.0f);
QSSGSceneDesc::setProperty(target, "quadraticFade", &QQuick3DSpotLight::setQuadraticFade, source.mAttenuationQuadratic * 10000.0f);
QSSGSceneDesc::setProperty(target, "coneAngle", &QQuick3DSpotLight::setConeAngle, qRadiansToDegrees(source.mAngleOuterCone) * 2.0f);
QSSGSceneDesc::setProperty(target, "innerConeAngle", &QQuick3DSpotLight::setInnerConeAngle, qRadiansToDegrees(source.mAngleInnerCone) * 2.0f);
} else {
if (hasAttConstant)
QSSGSceneDesc::setProperty(target, "constantFade", &QQuick3DPointLight::setConstantFade, source.mAttenuationConstant);
QSSGSceneDesc::setProperty(target, "linearFade", &QQuick3DPointLight::setLinearFade, source.mAttenuationLinear * 100.0f);
QSSGSceneDesc::setProperty(target, "quadraticFade", &QQuick3DPointLight::setQuadraticFade, source.mAttenuationQuadratic * 10000.0f);
}
}
// castShadow
// shadowBias
// shadowFactor
// shadowMapResolution
// shadowMapFar
// shadowMapFieldOfView
// shadowFilter
}
using MorphAttributes = QQuick3DMorphTarget::MorphTargetAttributes;
struct MorphProperty {
QByteArray name;
MorphAttributes attrib;
float weight;
};
static void setModelProperties(QSSGSceneDesc::Model &target, const aiNode &source, const SceneInfo &sceneInfo)
{
if (source.mNumMeshes == 0)
return;
auto &targetScene = target.scene;
const auto &srcScene = sceneInfo.scene;
// TODO: Correction and scale
setNodeProperties(target, source, sceneInfo, nullptr);
auto &meshStorage = targetScene->meshStorage;
auto &materialMap = sceneInfo.materialMap;
auto &meshMap = sceneInfo.meshMap;
auto &skinMap = sceneInfo.skinMap;
auto &mesh2skin = sceneInfo.mesh2skin;
QVarLengthArray<QSSGSceneDesc::Material *> materials;
materials.reserve(source.mNumMeshes); // Assumig there's max one material per mesh.
QString errorString;
const auto ensureMaterial = [&](qsizetype materialIndex) {
// Get the material for the mesh
auto &material = materialMap[materialIndex];
// Check if we need to create a new scene node for this material
auto targetMat = material.second;
if (targetMat == nullptr) {
const aiMaterial *sourceMat = material.first;
auto currentMaterialType = QSSGSceneDesc::Material::RuntimeType::PrincipledMaterial;
ai_real glossinessFactor;
aiReturn result = sourceMat->Get(AI_MATKEY_GLOSSINESS_FACTOR, glossinessFactor);
if (result == aiReturn_SUCCESS)
currentMaterialType = QSSGSceneDesc::Material::RuntimeType::SpecularGlossyMaterial;
targetMat = new QSSGSceneDesc::Material(currentMaterialType);
QSSGSceneDesc::addNode(target, *targetMat);
setMaterialProperties(*targetMat, *sourceMat, sceneInfo, currentMaterialType);
material.second = targetMat;
}
Q_ASSERT(targetMat != nullptr && material.second != nullptr);
// If these don't match then somethings broken...
Q_ASSERT(srcScene.mMaterials[materialIndex] == material.first);
materials.push_back(targetMat);
};
AssimpUtils::MeshList meshes;
qint16 skinIdx = -1;
// Combine all the meshes referenced by this model into a single MultiMesh file
// For the morphing, the target mesh must have the same AnimMeshes.
// It means if only one mesh has a morphing animation, the other sub-meshes will
// get null target attributes. However this case might not be common.
// These submeshes will animate with the same morphing weight!
// If meshes have separate skins, they should not be combined. GLTF2 does not
// seem to have problems related with this case, but When we use runtime asset
// for other formats, this case must be checked again.
// Here, we will use only the first skin in the mesh list
const auto combineMeshes = [&](const aiNode &source, aiMesh **sceneMeshes) {
for (qsizetype i = 0, end = source.mNumMeshes; i != end; ++i) {
const aiMesh &mesh = *sceneMeshes[source.mMeshes[i]];
ensureMaterial(mesh.mMaterialIndex);
if (skinIdx == -1 && mesh.HasBones())
skinIdx = mesh2skin[source.mMeshes[i]];
meshes.push_back(&mesh);
}
};
const auto createMeshNode = [&](const aiString &name) {
auto meshData = AssimpUtils::generateMeshData(srcScene,
meshes,
sceneInfo.opt.useFloatJointIndices,
sceneInfo.opt.generateMeshLODs,
sceneInfo.opt.lodNormalMergeAngle,
sceneInfo.opt.lodNormalSplitAngle,
errorString);
meshStorage.push_back(std::move(meshData));
const auto idx = meshStorage.size() - 1;
// For multimeshes we'll use the model name, but for single meshes we'll use the mesh name.
return new QSSGSceneDesc::Mesh(fromAiString(name), idx);
};
QSSGSceneDesc::Mesh *meshNode = nullptr;
const bool isMultiMesh = (source.mNumMeshes > 1);
if (isMultiMesh) {
// result is stored in 'meshes'
combineMeshes(source, srcScene.mMeshes);
Q_ASSERT(!meshes.isEmpty());
meshNode = createMeshNode(source.mName);
QSSGSceneDesc::addNode(target, *meshNode);
} else { // single mesh (We shouldn't be here if there are no meshes...)
Q_ASSERT(source.mNumMeshes == 1);
auto &mesh = meshMap[*source.mMeshes];
meshNode = mesh.second;
if (meshNode == nullptr) {
meshes = {mesh.first};
if (mesh.first->HasBones())
skinIdx = mesh2skin[*source.mMeshes];
mesh.second = meshNode = createMeshNode(mesh.first->mName);
QSSGSceneDesc::addNode(target, *meshNode); // We only add this the first time we create it.
}
ensureMaterial(mesh.first->mMaterialIndex);
Q_ASSERT(meshNode != nullptr && mesh.second != nullptr);
}
if (meshNode)
QSSGSceneDesc::setProperty(target, "source", &QQuick3DModel::setSource, QVariant::fromValue(meshNode));
if (skinIdx != -1) {
auto &skin = skinMap[skinIdx];
skin.node = new QSSGSceneDesc::Skin;
QSSGSceneDesc::setProperty(target, "skin", &QQuick3DModel::setSkin, skin.node);
QSSGSceneDesc::addNode(target, *skin.node);
// Skins' properties wil be set after all the nodes are processed
}
// materials
// Note that we use a QVector/QList here instead of a QQmlListProperty, as that would be really inconvenient.
// Since we don't create any runtime objects at this point, the list also contains the node type that corresponds with the
// type expected to be in the list (this is ensured at compile-time).
QSSGSceneDesc::setProperty(target, "materials", &QQuick3DModel::materials, materials);
}
static QSSGSceneDesc::Node *createSceneNode(const NodeInfo &nodeInfo,
const aiNode &srcNode,
QSSGSceneDesc::Node &parent,
const SceneInfo &sceneInfo)
{
QSSGSceneDesc::Node *node = nullptr;
const auto &srcScene = sceneInfo.scene;
switch (nodeInfo.type) {
case QSSGSceneDesc::Node::Type::Camera:
{
const auto &srcType = *srcScene.mCameras[nodeInfo.index];
// We set the initial rt-type to 'Custom', but we'll change it when updateing the properties.
auto targetType = new QSSGSceneDesc::Camera(QSSGSceneDesc::Node::RuntimeType::CustomCamera);
QSSGSceneDesc::addNode(parent, *targetType);
setCameraProperties(*targetType, srcType, srcNode, sceneInfo);
node = targetType;
}
break;
case QSSGSceneDesc::Node::Type::Light:
{
const auto &srcType = *srcScene.mLights[nodeInfo.index];
// Initial type is DirectonalLight, but will be change (if needed) when setting the properties.
auto targetType = new QSSGSceneDesc::Light(QSSGSceneDesc::Node::RuntimeType::DirectionalLight);
QSSGSceneDesc::addNode(parent, *targetType);
setLightProperties(*targetType, srcType, srcNode, sceneInfo);
node = targetType;
}
break;
case QSSGSceneDesc::Node::Type::Model:
{
auto target = new QSSGSceneDesc::Model;
QSSGSceneDesc::addNode(parent, *target);
setModelProperties(*target, srcNode, sceneInfo);
node = target;
}
break;
case QSSGSceneDesc::Node::Type::Joint:
{
auto target = new QSSGSceneDesc::Joint;
QSSGSceneDesc::addNode(parent, *target);
setNodeProperties(*target, srcNode, sceneInfo, nullptr);
QSSGSceneDesc::setProperty(*target, "index", &QQuick3DJoint::setIndex, qint32(nodeInfo.index));
node = target;
}
break;
case QSSGSceneDesc::Node::Type::Transform:
{
node = new QSSGSceneDesc::Node(QSSGSceneDesc::Node::Type::Transform, QSSGSceneDesc::Node::RuntimeType::Node);
QSSGSceneDesc::addNode(parent, *node);
// TODO: arguments for correction
setNodeProperties(*node, srcNode, sceneInfo, nullptr);
}
break;
default:
break;
}
return node;
}
static void processNode(const SceneInfo &sceneInfo, const aiNode &source, QSSGSceneDesc::Node &parent, const NodeMap &nodeMap, AnimationNodeMap &animationNodes)
{
QSSGSceneDesc::Node *node = nullptr;
if (source.mNumMeshes != 0) {
// Process morphTargets first and then add them to the modelNode
using It = decltype(source.mNumMeshes);
QVarLengthArray<MorphProperty> morphProps;
for (It i = 0, end = source.mNumMeshes; i != end; ++i) {
const auto &srcScene = sceneInfo.scene;
const aiMesh &mesh = *srcScene.mMeshes[source.mMeshes[i]];
const quint32 numMorphTargets = mesh.mNumAnimMeshes;
if (numMorphTargets && mesh.mAnimMeshes) {
morphProps.reserve(numMorphTargets);
for (uint j = 0; j < numMorphTargets; ++j) {
const auto &animMesh = mesh.mAnimMeshes[j];
MorphAttributes attrib;
if (animMesh->HasPositions())
attrib |= QQuick3DMorphTarget::MorphTargetAttribute::Position;
if (animMesh->HasNormals())
attrib |= QQuick3DMorphTarget::MorphTargetAttribute::Normal;
if (animMesh->HasTangentsAndBitangents()) {
attrib |= QQuick3DMorphTarget::MorphTargetAttribute::Tangent;
attrib |= QQuick3DMorphTarget::MorphTargetAttribute::Binormal;
}
morphProps.append({ fromAiString(animMesh->mName),
attrib,
animMesh->mWeight });
}
}
}
node = createSceneNode(NodeInfo { 0, QSSGSceneDesc::Node::Type::Model }, source, parent, sceneInfo);
if (!morphProps.isEmpty()) {
const QString nodeName(source.mName.C_Str());
QVarLengthArray<QSSGSceneDesc::MorphTarget *> morphTargets;
morphTargets.reserve(morphProps.size());
for (int i = 0, end = morphProps.size(); i != end; ++i) {
const auto morphProp = morphProps.at(i);
auto morphNode = new QSSGSceneDesc::MorphTarget;
QSSGSceneDesc::addNode(*node, *morphNode);
QSSGSceneDesc::setProperty(*morphNode, "weight", &QQuick3DMorphTarget::setWeight, morphProp.weight);
QSSGSceneDesc::setProperty(*morphNode, "attributes", &QQuick3DMorphTarget::setAttributes, morphProp.attrib);
morphNode->name = morphProp.name;
morphTargets.push_back(morphNode);
if (!animationNodes.isEmpty()) {
QString morphTargetName = nodeName + QStringLiteral("_morph") + QString::number(i);
const auto aNodeIt = animationNodes.find(morphTargetName.toUtf8());
if (aNodeIt != animationNodes.end() && aNodeIt.value() == nullptr)
*aNodeIt = morphNode;
}
}
QSSGSceneDesc::setProperty(*node, "morphTargets", &QQuick3DModel::morphTargets, morphTargets);
}
}
if (!node) {
NodeInfo nodeInfo{ 0, QSSGSceneDesc::Node::Type::Transform };
if (auto it = nodeMap.constFind(&source); it != nodeMap.constEnd())
nodeInfo = (*it);
node = createSceneNode(nodeInfo, source, parent, sceneInfo);
}
if (!node)
node = &parent;
Q_ASSERT(node->scene);
// Check if this node is a target for an animation
if (!animationNodes.isEmpty()) {
const auto &nodeName = source.mName;
auto aNodeIt = animationNodes.find(QByteArray{nodeName.C_Str(), qsizetype(nodeName.length)});
if (aNodeIt != animationNodes.end() && aNodeIt.value() == nullptr)
*aNodeIt = node;
}
// Process child nodes
using It = decltype (source.mNumChildren);
for (It i = 0, end = source.mNumChildren; i != end; ++i)
processNode(sceneInfo, **(source.mChildren + i), *node, nodeMap, animationNodes);
}
static QSSGSceneDesc::Animation::KeyPosition toAnimationKey(const aiVectorKey &key, qreal freq) {
const auto flag = quint16(QSSGSceneDesc::Animation::KeyPosition::KeyType::Time) | quint16(QSSGSceneDesc::Animation::KeyPosition::ValueType::Vec3);
return QSSGSceneDesc::Animation::KeyPosition { QVector4D{ key.mValue.x, key.mValue.y, key.mValue.z, 0.0f }, float(key.mTime * freq), flag };
}
static QSSGSceneDesc::Animation::KeyPosition toAnimationKey(const aiQuatKey &key, qreal freq) {
const auto flag = quint16(QSSGSceneDesc::Animation::KeyPosition::KeyType::Time) | quint16(QSSGSceneDesc::Animation::KeyPosition::ValueType::Quaternion);
return QSSGSceneDesc::Animation::KeyPosition { QVector4D{ key.mValue.x, key.mValue.y, key.mValue.z, key.mValue.w }, float(key.mTime * freq), flag };
}
static QSSGSceneDesc::Animation::KeyPosition toAnimationKey(const aiMeshMorphKey &key, qreal freq, uint morphId) {
const auto flag = quint16(QSSGSceneDesc::Animation::KeyPosition::KeyType::Time) | quint16(QSSGSceneDesc::Animation::KeyPosition::ValueType::Number);
return QSSGSceneDesc::Animation::KeyPosition { QVector4D{ float(key.mWeights[morphId]), 0.0f, 0.0f, 0.0f }, float(key.mTime * freq), flag };
}
static bool checkBooleanOption(const QString &optionName, const QJsonObject &options)
{
const auto it = options.constFind(optionName);
const auto end = options.constEnd();
QJsonValue value;
if (it != end) {
if (it->isObject())
value = it->toObject().value("value");
else
value = it.value();
}
return value.toBool();
}
static qreal getRealOption(const QString &optionName, const QJsonObject &options)
{
const auto it = options.constFind(optionName);
const auto end = options.constEnd();
QJsonValue value;
if (it != end) {
if (it->isObject())
value = it->toObject().value("value");
else
value = it.value();
}
return value.toDouble();
}
#define demonPostProcessPresets ( \
aiProcess_CalcTangentSpace | \
aiProcess_JoinIdenticalVertices | \
aiProcess_ImproveCacheLocality | \
aiProcess_RemoveRedundantMaterials | \
aiProcess_SplitLargeMeshes | \
aiProcess_Triangulate | \
aiProcess_GenUVCoords | \
aiProcess_SortByPType | \
aiProcess_FindDegenerates | \
aiProcess_FindInvalidData | \
0 )
static aiPostProcessSteps processOptions(const QJsonObject &optionsObject, std::unique_ptr<Assimp::Importer> &importer) {
aiPostProcessSteps postProcessSteps = aiPostProcessSteps(aiProcess_Triangulate | aiProcess_SortByPType);;
// 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 options = optionsObject;
if (auto it = options.constFind("options"), end = options.constEnd(); it != end)
options = it->toObject();
if (options.isEmpty())
return postProcessSteps;
// parse the options list for values
if (checkBooleanOption(QStringLiteral("calculateTangentSpace"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_CalcTangentSpace);
if (checkBooleanOption(QStringLiteral("joinIdenticalVertices"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_JoinIdenticalVertices);
if (checkBooleanOption(QStringLiteral("generateNormals"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_GenNormals);
if (checkBooleanOption(QStringLiteral("generateSmoothNormals"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_GenSmoothNormals);
if (checkBooleanOption(QStringLiteral("splitLargeMeshes"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_SplitLargeMeshes);
if (checkBooleanOption(QStringLiteral("preTransformVertices"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_PreTransformVertices);
if (checkBooleanOption(QStringLiteral("improveCacheLocality"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_ImproveCacheLocality);
if (checkBooleanOption(QStringLiteral("removeRedundantMaterials"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_RemoveRedundantMaterials);
if (checkBooleanOption(QStringLiteral("fixInfacingNormals"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_FixInfacingNormals);
if (checkBooleanOption(QStringLiteral("findDegenerates"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_FindDegenerates);
if (checkBooleanOption(QStringLiteral("findInvalidData"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_FindInvalidData);
if (checkBooleanOption(QStringLiteral("transformUVCoordinates"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_TransformUVCoords);
if (checkBooleanOption(QStringLiteral("findInstances"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_FindInstances);
if (checkBooleanOption(QStringLiteral("optimizeMeshes"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_OptimizeMeshes);
if (checkBooleanOption(QStringLiteral("optimizeGraph"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_OptimizeGraph);
if (checkBooleanOption(QStringLiteral("dropNormals"), options))
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_DropNormals);
aiComponent removeComponents = aiComponent(0);
if (checkBooleanOption(QStringLiteral("removeComponentNormals"), options))
removeComponents = aiComponent(removeComponents | aiComponent_NORMALS);
if (checkBooleanOption(QStringLiteral("removeComponentTangentsAndBitangents"), options))
removeComponents = aiComponent(removeComponents | aiComponent_TANGENTS_AND_BITANGENTS);
if (checkBooleanOption(QStringLiteral("removeComponentColors"), options))
removeComponents = aiComponent(removeComponents | aiComponent_COLORS);
if (checkBooleanOption(QStringLiteral("removeComponentUVs"), options))
removeComponents = aiComponent(removeComponents | aiComponent_TEXCOORDS);
if (checkBooleanOption(QStringLiteral("removeComponentBoneWeights"), options))
removeComponents = aiComponent(removeComponents | aiComponent_BONEWEIGHTS);
if (checkBooleanOption(QStringLiteral("removeComponentAnimations"), options))
removeComponents = aiComponent(removeComponents | aiComponent_ANIMATIONS);
if (checkBooleanOption(QStringLiteral("removeComponentTextures"), options))
removeComponents = aiComponent(removeComponents | aiComponent_TEXTURES);
if (removeComponents != aiComponent(0)) {
postProcessSteps = aiPostProcessSteps(postProcessSteps | aiProcess_RemoveComponent);
importer->SetPropertyInteger(AI_CONFIG_PP_RVC_FLAGS, removeComponents);
}
bool preservePivots = checkBooleanOption(QStringLiteral("fbxPreservePivots"), options);
importer->SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, preservePivots);
return postProcessSteps;
}
static SceneInfo::Options processSceneOptions(const QJsonObject &optionsObject) {
SceneInfo::Options sceneOptions;
// 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 options = optionsObject;
if (auto it = options.constFind("options"), end = options.constEnd(); it != end)
options = it->toObject();
if (options.isEmpty())
return sceneOptions;
if (checkBooleanOption(QStringLiteral("globalScale"), options)) {
sceneOptions.globalScaleValue = getRealOption(QStringLiteral("globalScaleValue"), options);
if (sceneOptions.globalScaleValue == 0.0)
sceneOptions.globalScaleValue = 1.0;
}
sceneOptions.designStudioWorkarounds = checkBooleanOption(QStringLiteral("designStudioWorkarounds"), options);
sceneOptions.useFloatJointIndices = checkBooleanOption(QStringLiteral("useFloatJointIndices"), options);
sceneOptions.forceMipMapGeneration = checkBooleanOption(QStringLiteral("generateMipMaps"), options);
sceneOptions.binaryKeyframes = checkBooleanOption(QStringLiteral("useBinaryKeyframes"), options);
sceneOptions.generateMeshLODs = checkBooleanOption(QStringLiteral("generateMeshLevelsOfDetail"), options);
if (sceneOptions.generateMeshLODs) {
bool recalculateLODNormals = checkBooleanOption(QStringLiteral("recalculateLodNormals"), options);
if (recalculateLODNormals) {
qreal mergeAngle = getRealOption(QStringLiteral("recalculateLodNormalsMergeAngle"), options);
sceneOptions.lodNormalMergeAngle = qBound(0.0, mergeAngle, 270.0);
qreal splitAngle = getRealOption(QStringLiteral("recalculateLodNormalsSplitAngle"), options);
sceneOptions.lodNormalSplitAngle = qBound(0.0, splitAngle, 270.0);
} else {
sceneOptions.lodNormalMergeAngle = 0.0;
sceneOptions.lodNormalSplitAngle = 0.0;
}
}
return sceneOptions;
}
static QString importImp(const QUrl &url, const QJsonObject &options, QSSGSceneDesc::Scene &targetScene)
{
auto filePath = url.path();
const bool maybeLocalFile = QQmlFile::isLocalFile(url);
if (maybeLocalFile && !QFileInfo::exists(filePath))
filePath = QQmlFile::urlToLocalFileOrQrc(url);
auto sourceFile = QFileInfo(filePath);
if (!sourceFile.exists())
return QLatin1String("File not found");
targetScene.sourceDir = sourceFile.path();
std::unique_ptr<Assimp::Importer> importer(new Assimp::Importer());
// Setup import from Options
aiPostProcessSteps postProcessSteps;
if (options.isEmpty())
postProcessSteps = aiPostProcessSteps(demonPostProcessPresets);
else
postProcessSteps = processOptions(options, importer);
// Remove primitives that are not Triangles
importer->SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_POINT | aiPrimitiveType_LINE);
importer->SetPropertyInteger(AI_CONFIG_IMPORT_COLLADA_USE_COLLADA_NAMES, 1);
if (filePath.startsWith(":"))
importer->SetIOHandler(new ResourceIOSystem);
#ifdef Q_OS_WASM
// Always use QFile-based IO on wasm, so that URL schemes like
// weblocalfile: are handled by Qt's file engine abstraction.
else
importer->SetIOHandler(new ResourceIOSystem);
#endif
auto sourceScene = importer->ReadFile(filePath.toStdString(), postProcessSteps);
if (!sourceScene) {
// Scene failed to load, use logger to get the reason
return QString::fromLocal8Bit(importer->GetErrorString());
}
// For simplicity, and convenience, we'll just use the file path as the id.
// DO NOT USE it for anything else, once the scene is created there's no
// real connection to the source asset file.
targetScene.id = sourceFile.canonicalFilePath();
// Assuming consistent type usage
using It = decltype(sourceScene->mNumMeshes);
// Before we can start processing the scene we start my mapping out the nodes
// we can tell the type of.
const auto &srcRootNode = *sourceScene->mRootNode;
NodeMap nodeMap;
// We need to know which nodes are animated so we can map _our_ animation data to
// the target node (in Assimp this is string based mapping).
AnimationNodeMap animatingNodes;
{
if (sourceScene->HasLights()) {
for (It i = 0, end = sourceScene->mNumLights; i != end; ++i) {
const auto &type = *sourceScene->mLights[i];
if (auto node = srcRootNode.FindNode(type.mName))
nodeMap[node] = { i, NodeInfo::Type::Light };
}
}
if (sourceScene->HasCameras()) {
for (It i = 0, end = sourceScene->mNumCameras; i != end; ++i) {
const auto &srcCam = *sourceScene->mCameras[i];
if (auto node = srcRootNode.FindNode(srcCam.mName))
nodeMap[node] = { i, NodeInfo::Type::Camera };
}
}
if (sourceScene->HasAnimations()) {
for (It i = 0, end = sourceScene->mNumAnimations; i != end; ++i) {
const auto &srcAnim = *sourceScene->mAnimations[i];
const auto channelCount = srcAnim.mNumChannels;
for (It cIdx = 0; cIdx != channelCount; ++cIdx) {
const auto &srcChannel = srcAnim.mChannels[cIdx];
const auto &nodeName = srcChannel->mNodeName;
if (nodeName.length > 0) {
// We'll update this once we've created the node!
QByteArray name(nodeName.C_Str(), qsizetype(nodeName.length));
if (!animatingNodes.contains(name))
animatingNodes.insert(name, nullptr);
}
}
const auto morphChannelCount = srcAnim.mNumMorphMeshChannels;
for (It cIdx = 0; cIdx != morphChannelCount; ++cIdx) {
const auto &srcChannel = srcAnim.mMorphMeshChannels[cIdx];
const auto &nodeName = srcChannel->mName;
if (nodeName.length > 0) {
const auto morphKeys = srcChannel->mKeys;
const auto numMorphTargets = morphKeys[0].mNumValuesAndWeights;
// MorphTarget's animations are stored with a name,
// "<nodeName> + '_morph' + <targetNumber>"
for (It j = 0; j < numMorphTargets; ++j) {
QString morphTargetName(nodeName.C_Str());
morphTargetName += QStringLiteral("_morph") + QString::number(j);
animatingNodes.insert(morphTargetName.toUtf8(), nullptr);
}
}
}
}
}
}
// We'll use these to ensure we don't re-create resources.
const auto materialCount = sourceScene->mNumMaterials;
SceneInfo::MaterialMap materials;
materials.reserve(materialCount);
const auto meshCount = sourceScene->mNumMeshes;
SceneInfo::MeshMap meshes;
meshes.reserve(meshCount);
SceneInfo::Mesh2SkinMap mesh2skin;
mesh2skin.reserve(meshCount);
const auto embeddedTextureCount = sourceScene->mNumTextures;
SceneInfo::EmbeddedTextureMap embeddedTextures;
SceneInfo::SkinMap skins;
for (It i = 0; i != materialCount; ++i)
materials.push_back({sourceScene->mMaterials[i], nullptr});
for (It i = 0; i != meshCount; ++i) {
meshes.push_back({sourceScene->mMeshes[i], nullptr});
if (sourceScene->mMeshes[i]->HasBones()) {
mesh2skin.push_back(skins.size());
const auto boneCount = sourceScene->mMeshes[i]->mNumBones;
auto bones = sourceScene->mMeshes[i]->mBones;
skins.push_back(SceneInfo::skinData{ bones, boneCount, nullptr });
// For skinning, we need to get the joints list and their target nodes.
// It is also done by the string based mapping and many of them will
// be animated. So we will use existing AnimationNodeMap for the data.
for (It j = 0; j != boneCount; ++j) {
const auto &nodeName = bones[j]->mName;
if (nodeName.length > 0) {
animatingNodes.insert(QByteArray{ nodeName.C_Str(),
qsizetype(nodeName.length) },
nullptr);
}
}
} else {
mesh2skin.push_back(-1);
}
}
for (It i = 0; i != embeddedTextureCount; ++i)
embeddedTextures.push_back(nullptr);
SceneInfo::TextureMap textureMap;
if (!targetScene.root) {
auto root = new QSSGSceneDesc::Node(QByteArrayLiteral("Root"), QSSGSceneDesc::Node::Type::Transform, QSSGSceneDesc::Node::RuntimeType::Node);
QSSGSceneDesc::addNode(targetScene, *root);
}
// Get Options
auto opt = processSceneOptions(options);
// check if the asset is GLTF format
const auto extension = sourceFile.suffix().toLower();
if (extension == QStringLiteral("gltf") || extension == QStringLiteral("glb"))
opt.gltfMode = true;
else if (extension == QStringLiteral("fbx"))
opt.fbxMode = true;
SceneInfo sceneInfo { *sourceScene, materials, meshes, embeddedTextures,
textureMap, skins, mesh2skin, sourceFile.dir(), opt };
if (!qFuzzyCompare(opt.globalScaleValue, 1.0f) && !qFuzzyCompare(opt.globalScaleValue, 0.0f)) {
const auto gscale = opt.globalScaleValue;
QSSGSceneDesc::setProperty(*targetScene.root, "scale", &QQuick3DNode::setScale, QVector3D { gscale, gscale, gscale });
}
// Now lets go through the scene
if (sourceScene->mRootNode)
processNode(sceneInfo, *sourceScene->mRootNode, *targetScene.root, nodeMap, animatingNodes);
// skins
for (It i = 0, endI = skins.size(); i != endI; ++i) {
const auto &skin = skins[i];
// It is possible that an asset has a unused mesh with a skin
if (!skin.node)
continue;
QList<QMatrix4x4> inverseBindPoses;
QVarLengthArray<QSSGSceneDesc::Node *> joints;
joints.reserve(skin.mNumBones);
for (It j = 0, endJ = skin.mNumBones; j != endJ; ++j) {
const auto &bone = *skin.mBones[j];
const auto &nodeName = bone.mName;
if (nodeName.length > 0) {
auto targetNode = animatingNodes.value(QByteArray{ nodeName.C_Str(), qsizetype(nodeName.length) });
joints.push_back(targetNode);
const auto &osMat = bone.mOffsetMatrix;
auto pose = QMatrix4x4(osMat[0][0], osMat[0][1], osMat[0][2], osMat[0][3],
osMat[1][0], osMat[1][1], osMat[1][2], osMat[1][3],
osMat[2][0], osMat[2][1], osMat[2][2], osMat[2][3],
osMat[3][0], osMat[3][1], osMat[3][2], osMat[3][3]);
inverseBindPoses.push_back(pose);
}
}
QSSGSceneDesc::setProperty(*skin.node, "joints", &QQuick3DSkin::joints, joints);
QSSGSceneDesc::setProperty(*skin.node, "inverseBindPoses", &QQuick3DSkin::setInverseBindPoses, inverseBindPoses);
}
static const auto fuzzyComparePos = [](const aiVectorKey *pos, const aiVectorKey *prev){
if (!prev)
return false;
return qFuzzyCompare(pos->mValue.x, prev->mValue.x)
&& qFuzzyCompare(pos->mValue.y, prev->mValue.y)
&& qFuzzyCompare(pos->mValue.z, prev->mValue.z);
};
static const auto fuzzyCompareRot = [](const aiQuatKey *rot, const aiQuatKey *prev){
if (!prev)
return false;
return qFuzzyCompare(rot->mValue.x, prev->mValue.x)
&& qFuzzyCompare(rot->mValue.y, prev->mValue.y)
&& qFuzzyCompare(rot->mValue.z, prev->mValue.z)
&& qFuzzyCompare(rot->mValue.w, prev->mValue.w);
};
static const auto createAnimation = [](QSSGSceneDesc::Scene &targetScene, const aiAnimation &srcAnim, const AnimationNodeMap &animatingNodes) {
using namespace QSSGSceneDesc;
Animation targetAnimation;
auto &channels = targetAnimation.channels;
qreal freq = qFuzzyIsNull(srcAnim.mTicksPerSecond) ? 1.0
: 1000.0 / srcAnim.mTicksPerSecond;
targetAnimation.framesPerSecond = srcAnim.mTicksPerSecond;
targetAnimation.name = fromAiString(srcAnim.mName);
// Process property channels
for (It i = 0, end = srcAnim.mNumChannels; i != end; ++i) {
const auto &srcChannel = *srcAnim.mChannels[i];
const auto &nodeName = srcChannel.mNodeName;
if (nodeName.length > 0) {
const auto aNodeEnd = animatingNodes.cend();
const auto aNodeIt = animatingNodes.constFind(QByteArray{ nodeName.C_Str(), qsizetype(nodeName.length) });
if (aNodeIt != aNodeEnd && aNodeIt.value() != nullptr) {
auto targetNode = aNodeIt.value();
// Target propert[y|ies]
const auto currentPropertyValue = [targetNode](const char *propertyName) -> QVariant {
for (const auto *p : std::as_const(targetNode->properties)) {
if (!qstrcmp(propertyName, p->name))
return p->value;
}
return {};
};
{ // Position
const auto posKeyEnd = srcChannel.mNumPositionKeys;
Animation::Channel targetChannel;
targetChannel.targetProperty = Animation::Channel::TargetProperty::Position;
targetChannel.target = targetNode;
const aiVectorKey *prevPos = nullptr;
for (It posKeyIdx = 0; posKeyIdx != posKeyEnd; ++posKeyIdx) {
const auto &posKey = srcChannel.mPositionKeys[posKeyIdx];
if (fuzzyComparePos(&posKey, prevPos))
continue;
targetChannel.keys.push_back(new Animation::KeyPosition(toAnimationKey(posKey, freq)));
prevPos = &posKey;
}
const auto isUnchanged = [&targetChannel, currentPropertyValue]() {
if (targetChannel.keys.count() != 1)
return false;
auto currentPos = currentPropertyValue("position").value<QVector3D>();
return qFuzzyCompare(targetChannel.keys[0]->value.toVector3D(), currentPos);
};
if (!targetChannel.keys.isEmpty()) {
if (!isUnchanged()) {
channels.push_back(new Animation::Channel(targetChannel));
float endTime = float(srcChannel.mPositionKeys[posKeyEnd - 1].mTime) * freq;
if (targetAnimation.length < endTime)
targetAnimation.length = endTime;
} else {
// the keys will not be used.
qDeleteAll(targetChannel.keys);
}
}
}
{ // Rotation
const auto rotKeyEnd = srcChannel.mNumRotationKeys;
Animation::Channel targetChannel;
targetChannel.targetProperty = Animation::Channel::TargetProperty::Rotation;
targetChannel.target = targetNode;
const aiQuatKey *prevRot = nullptr;
for (It rotKeyIdx = 0; rotKeyIdx != rotKeyEnd; ++rotKeyIdx) {
const auto &rotKey = srcChannel.mRotationKeys[rotKeyIdx];
if (fuzzyCompareRot(&rotKey, prevRot))
continue;
targetChannel.keys.push_back(new Animation::KeyPosition(toAnimationKey(rotKey, freq)));
prevRot = &rotKey;
}
const auto isUnchanged = [&targetChannel, currentPropertyValue]() {
if (targetChannel.keys.count() != 1)
return false;
auto currentVal = currentPropertyValue("rotation");
QQuaternion rot = currentVal.isValid() ? currentVal.value<QQuaternion>() : QQuaternion{};
return qFuzzyCompare(QQuaternion(targetChannel.keys[0]->value), rot);
};
if (!targetChannel.keys.isEmpty()) {
if (!isUnchanged()) {
channels.push_back(new Animation::Channel(targetChannel));
float endTime = float(srcChannel.mRotationKeys[rotKeyEnd - 1].mTime) * freq;
if (targetAnimation.length < endTime)
targetAnimation.length = endTime;
} else {
// the keys will not be used.
qDeleteAll(targetChannel.keys);
}
}
}
{ // Scale
const auto scaleKeyEnd = srcChannel.mNumScalingKeys;
Animation::Channel targetChannel;
targetChannel.targetProperty = Animation::Channel::TargetProperty::Scale;
targetChannel.target = targetNode;
const aiVectorKey *prevScale = nullptr;
for (It scaleKeyIdx = 0; scaleKeyIdx != scaleKeyEnd; ++scaleKeyIdx) {
const auto &scaleKey = srcChannel.mScalingKeys[scaleKeyIdx];
if (fuzzyComparePos(&scaleKey, prevScale))
continue;
targetChannel.keys.push_back(new Animation::KeyPosition(toAnimationKey(scaleKey, freq)));
prevScale = &scaleKey;
}
const auto isUnchanged = [&targetChannel, currentPropertyValue]() {
if (targetChannel.keys.count() != 1)
return false;
auto currentVal = currentPropertyValue("scale");
QVector3D scale = currentVal.isValid() ? currentVal.value<QVector3D>() : QVector3D{ 1, 1, 1 };
return qFuzzyCompare(targetChannel.keys[0]->value.toVector3D(), scale);
};
if (!targetChannel.keys.isEmpty()) {
if (!isUnchanged()) {
channels.push_back(new Animation::Channel(targetChannel));
float endTime = float(srcChannel.mScalingKeys[scaleKeyEnd - 1].mTime) * freq;
if (targetAnimation.length < endTime)
targetAnimation.length = endTime;
} else {
// the keys will not be used.
qDeleteAll(targetChannel.keys);
}
}
}
}
}
}
// Morphing Animations
for (It i = 0, end = srcAnim.mNumMorphMeshChannels; i != end; ++i) {
const auto &srcMorphChannel = *srcAnim.mMorphMeshChannels[i];
const QString nodeName(srcMorphChannel.mName.C_Str());
const auto *morphKeys = srcMorphChannel.mKeys;
const auto numMorphTargets = qMin(morphKeys[0].mNumValuesAndWeights, 8U);
for (It targetId = 0; targetId != numMorphTargets; ++targetId) {
QString morphTargetName = nodeName + QStringLiteral("_morph") + QString::number(targetId);
const auto aNodeEnd = animatingNodes.cend();
const auto aNodeIt = animatingNodes.constFind(morphTargetName.toUtf8());
if (aNodeIt != aNodeEnd && aNodeIt.value() != nullptr) {
auto targetNode = aNodeIt.value();
const auto weightKeyEnd = srcMorphChannel.mNumKeys;
Animation::Channel targetChannel;
targetChannel.targetProperty = Animation::Channel::TargetProperty::Weight;
targetChannel.target = targetNode;
for (It wId = 0; wId != weightKeyEnd; ++wId) {
const auto &weightKey = srcMorphChannel.mKeys[wId];
const auto animationKey = new Animation::KeyPosition(toAnimationKey(weightKey, freq, targetId));
targetChannel.keys.push_back(animationKey);
}
if (!targetChannel.keys.isEmpty()) {
channels.push_back(new Animation::Channel(targetChannel));
float endTime = float(srcMorphChannel.mKeys[weightKeyEnd - 1].mTime) * freq;
if (targetAnimation.length < endTime)
targetAnimation.length = endTime;
}
}
}
}
// If we have data we need to make it persistent.
if (!targetAnimation.channels.isEmpty())
targetScene.animations.push_back(new Animation(targetAnimation));
};
// All scene nodes should now be created (and ready), so let's go through the animation data.
if (sourceScene->HasAnimations()) {
const auto animationCount = sourceScene->mNumAnimations;
targetScene.animations.reserve(animationCount);
for (It i = 0, end = animationCount; i != end; ++i) {
const auto &srcAnim = *sourceScene->mAnimations[i];
createAnimation(targetScene, srcAnim, animatingNodes);
}
}
// TODO, FIX: Editing the scene after the import ought to be done by QSSGAssetImportManager
// and not by the asset import plugin. However, the asset import module cannot use
// the asset utils module because that would cause a circular dependency. This
// needs a deeper architectural fix.
QSSGQmlUtilities::applyEdit(&targetScene, options);
return QString();
}
////////////////////////
QString AssimpImporter::import(const QUrl &url, const QJsonObject &options, QSSGSceneDesc::Scene &scene)
{
// We'll simply use assimp to load the scene and then translate the Aassimp scene
// into our own format.
return importImp(url, options, scene);
}
QString AssimpImporter::import(const QString &sourceFile, const QDir &savePath, const QJsonObject &options, QStringList *generatedFiles)
{
QString errorString;
QSSGSceneDesc::Scene scene;
// Load scene data
auto sourceUrl = QUrl::fromLocalFile(sourceFile);
errorString = importImp(sourceUrl, options, scene);
if (!errorString.isEmpty())
return errorString;
// Write out QML + Resources
QFileInfo sourceFileInfo(sourceFile);
QString targetFileName = savePath.absolutePath() + QDir::separator() +
QSSGQmlUtilities::qmlComponentName(sourceFileInfo.completeBaseName()) +
QStringLiteral(".qml");
QFile targetFile(targetFileName);
if (!targetFile.open(QIODevice::WriteOnly)) {
errorString += QString("Could not write to file: ") + targetFileName;
} else {
QTextStream output(&targetFile);
QSSGQmlUtilities::writeQml(scene, output, savePath, options);
if (generatedFiles)
generatedFiles->append(targetFileName);
}
scene.cleanup();
return errorString;
}
QT_END_NAMESPACE