/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the Qt3D module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ ** 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 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define GLT_UNSIGNED_SHORT 0x1403 #define GLT_UNSIGNED_INT 0x1405 #define GLT_FLOAT 0x1406 #define GLT_FLOAT_VEC2 0x8B50 #define GLT_FLOAT_VEC3 0x8B51 #define GLT_FLOAT_VEC4 0x8B52 #define GLT_FLOAT_MAT3 0x8B5B #define GLT_FLOAT_MAT4 0x8B5C #define GLT_SAMPLER_2D 0x8B5E #define GLT_ARRAY_BUFFER 0x8892 #define GLT_ELEMENT_ARRAY_BUFFER 0x8893 #define GLT_DEPTH_TEST 0x0B71 #define GLT_CULL_FACE 0x0B44 #define GLT_BLEND 0x0BE2 class AssimpIOStream : public Assimp::IOStream { public: AssimpIOStream(QIODevice *device); ~AssimpIOStream(); 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: QIODevice *m_device; }; class AssimpIOSystem : public Assimp::IOSystem { public: 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; }; AssimpIOStream::AssimpIOStream(QIODevice *device) : m_device(device) { Q_ASSERT(m_device); } AssimpIOStream::~AssimpIOStream() { delete m_device; } size_t AssimpIOStream::Read(void *pvBuffer, size_t pSize, size_t pCount) { qint64 readBytes = m_device->read((char *)pvBuffer, pSize * pCount); if (readBytes < 0) qWarning() << Q_FUNC_INFO << " read failed"; return readBytes; } size_t AssimpIOStream::Write(const void *pvBuffer, size_t pSize, size_t pCount) { qint64 writtenBytes = m_device->write((char *)pvBuffer, pSize * pCount); if (writtenBytes < 0) qWarning() << Q_FUNC_INFO << " write failed"; return writtenBytes; } aiReturn AssimpIOStream::Seek(size_t pOffset, aiOrigin pOrigin) { qint64 seekPos = pOffset; if (pOrigin == aiOrigin_CUR) seekPos += m_device->pos(); else if (pOrigin == aiOrigin_END) seekPos += m_device->size(); if (!m_device->seek(seekPos)) { qWarning() << Q_FUNC_INFO << " seek failed"; return aiReturn_FAILURE; } return aiReturn_SUCCESS; } size_t AssimpIOStream::Tell() const { return m_device->pos(); } size_t AssimpIOStream::FileSize() const { return m_device->size(); } void AssimpIOStream::Flush() { // we don't write via assimp } static QIODevice::OpenMode openModeFromText(const char *name) noexcept { static const struct OpenModeMapping { char name[2]; int mode; } openModeMapping[] = { { { 'r', 0 }, QIODevice::ReadOnly }, { { 'r', '+' }, QIODevice::ReadWrite }, { { 'w', 0 }, QIODevice::WriteOnly | QIODevice::Truncate }, { { 'w', '+' }, QIODevice::ReadWrite | QIODevice::Truncate }, { { 'a', 0 }, QIODevice::WriteOnly | QIODevice::Append }, { { 'a', '+' }, QIODevice::ReadWrite | QIODevice::Append }, { { 'w', 'b' }, QIODevice::WriteOnly }, { { 'w', 't' }, QIODevice::WriteOnly | QIODevice::Text }, { { 'r', 'b' }, QIODevice::ReadOnly }, { { 'r', 't' }, QIODevice::ReadOnly | QIODevice::Text }, }; for (auto e : openModeMapping) { if (qstrncmp(e.name, name, sizeof(OpenModeMapping::name)) == 0) return static_cast(e.mode); } return QIODevice::NotOpen; } bool AssimpIOSystem::Exists(const char *pFile) const { return QFileInfo::exists(QString::fromUtf8(pFile)); } char AssimpIOSystem::getOsSeparator() const { return QDir::separator().toLatin1(); } Assimp::IOStream *AssimpIOSystem::Open(const char *pFile, const char *pMode) { const QString fileName(QString::fromUtf8(pFile)); const QLatin1String cleanedMode = QLatin1String{pMode}.trimmed(); if (const QIODevice::OpenMode openMode = openModeFromText(cleanedMode.data())) { QScopedPointer file(new QFile(fileName)); if (file->open(openMode)) return new AssimpIOStream(file.take()); } return nullptr; } void AssimpIOSystem::Close(Assimp::IOStream *pFile) { delete pFile; } static inline QString ai2qt(const aiString &str) { return QString::fromUtf8(str.data, int(str.length)); } static inline QVector ai2qt(const aiMatrix4x4 &matrix) { return QVector() << matrix.a1 << matrix.b1 << matrix.c1 << matrix.d1 << matrix.a2 << matrix.b2 << matrix.c2 << matrix.d2 << matrix.a3 << matrix.b3 << matrix.c3 << matrix.d3 << matrix.a4 << matrix.b4 << matrix.c4 << matrix.d4; } struct Options { QString outDir; bool genBin; bool compact; bool compress; bool genTangents; bool interleave; float scale; bool genCore; enum TextureCompression { NoTextureCompression, ETC1 }; TextureCompression texComp; bool commonMat; bool shaders; bool showLog; } opts; class Importer { public: Importer(); virtual ~Importer(); virtual bool load(const QString &filename) = 0; struct BufferInfo { QString name; QByteArray data; }; QVector buffers() const; struct MeshInfo { struct BufferView { BufferView() : bufIndex(0), offset(0), length(0), componentType(0), target(0) { } QString name; uint bufIndex; uint offset; uint length; uint componentType; uint target; }; QVector views; struct Accessor { Accessor() : offset(0), stride(0), count(0), componentType(0) { } QString name; QString usage; QString bufferView; uint offset; uint stride; uint count; uint componentType; QString type; QVector minVal; QVector maxVal; }; QVector accessors; QString name; // generated QString originalName; // may be empty uint materialIndex; }; QVector bufferViews() const; QVector accessors() const; uint meshCount() const; MeshInfo meshInfo(uint meshIndex) const; struct MaterialInfo { QString name; QString originalName; QHash > m_colors; QHash m_values; QHash m_textures; }; uint materialCount() const; MaterialInfo materialInfo(uint materialIndex) const; QSet externalTextures() const; struct CameraInfo { QString name; // suffixed float aspectRatio; float yfov; float zfar; float znear; }; QHash cameraInfo() const; struct EmbeddedTextureInfo { EmbeddedTextureInfo() { } QString name; #ifdef HAS_QIMAGE EmbeddedTextureInfo(const QString &name, const QImage &image) : name(name), image(image) { } QImage image; #endif }; QHash embeddedTextures() const; struct Node { QString name; QString uniqueName; // generated QVector transformation; QVector children; QVector meshes; }; const Node *rootNode() const; struct KeyFrame { KeyFrame() : t(0), transValid(false), rotValid(false), scaleValid(false) { } float t; bool transValid; QVector trans; bool rotValid; QVector rot; bool scaleValid; QVector scale; }; struct AnimationInfo { AnimationInfo() : hasTranslation(false), hasRotation(false), hasScale(false) { } QString name; QString targetNode; bool hasTranslation; bool hasRotation; bool hasScale; QVector keyFrames; }; QVector animations() const; bool allMeshesForMaterialHaveTangents(uint materialIndex) const; const Node *findNode(const Node *root, const QString &originalName) const; protected: void delNode(Importer::Node *n); QByteArray m_buffer; QHash m_meshInfo; QHash m_materialInfo; QHash m_embeddedTextures; QSet m_externalTextures; QHash m_cameraInfo; Node *m_rootNode; QVector m_animations; }; QT_BEGIN_NAMESPACE Q_DECLARE_TYPEINFO(Importer::BufferInfo, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::MeshInfo::BufferView, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::MeshInfo::Accessor, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::MaterialInfo, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::CameraInfo, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::EmbeddedTextureInfo, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::Node, Q_COMPLEX_TYPE); // uses address as identity Q_DECLARE_TYPEINFO(Importer::KeyFrame, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(Importer::AnimationInfo, Q_MOVABLE_TYPE); QT_END_NAMESPACE Importer::Importer() : m_rootNode(nullptr) { } void Importer::delNode(Importer::Node *n) { if (!n) return; for (Importer::Node *c : qAsConst(n->children)) delNode(c); delete n; } Importer::~Importer() { delNode(m_rootNode); } QVector Importer::buffers() const { BufferInfo b; b.name = QStringLiteral("buf"); b.data = m_buffer; return QVector() << b; } const Importer::Node *Importer::rootNode() const { return m_rootNode; } bool Importer::allMeshesForMaterialHaveTangents(uint materialIndex) const { for (const MeshInfo &mi : m_meshInfo) { if (mi.materialIndex == materialIndex) { bool hasTangents = false; for (const MeshInfo::Accessor &acc : mi.accessors) { if (acc.usage == QStringLiteral("TANGENT")) { hasTangents = true; break; } } if (!hasTangents) return false; } } return true; } QVector Importer::bufferViews() const { QVector bv; for (const MeshInfo &mi : m_meshInfo) { for (const MeshInfo::BufferView &v : mi.views) bv << v; } return bv; } QVector Importer::accessors() const { QVector acc; for (const MeshInfo &mi : m_meshInfo) { for (const MeshInfo::Accessor &a : mi.accessors) acc << a; } return acc; } uint Importer::meshCount() const { return m_meshInfo.count(); } Importer::MeshInfo Importer::meshInfo(uint meshIndex) const { return m_meshInfo[meshIndex]; } uint Importer::materialCount() const { return m_materialInfo.count(); } Importer::MaterialInfo Importer::materialInfo(uint materialIndex) const { return m_materialInfo[materialIndex]; } QHash Importer::cameraInfo() const { return m_cameraInfo; } QSet Importer::externalTextures() const { return m_externalTextures; } QHash Importer::embeddedTextures() const { return m_embeddedTextures; } QVector Importer::animations() const { return m_animations; } const Importer::Node *Importer::findNode(const Node *root, const QString &originalName) const { for (const Node *c : root->children) { if (c->name == originalName) return c; const Node *cn = findNode(c, originalName); if (cn) return cn; } return nullptr; } class AssimpImporter : public Importer { public: AssimpImporter(); bool load(const QString &filename) override; private: const aiScene *scene() const; void printNodes(const aiNode *node, int level = 1); void buildBuffer(); void parseEmbeddedTextures(); void parseMaterials(); void parseCameras(); void parseNode(Importer::Node *dst, const aiNode *src); void parseScene(); void parseAnimations(); void addKeyFrame(QVector &keyFrames, float t, aiVector3D *vt, aiQuaternion *vr, aiVector3D *vs); QScopedPointer m_importer; }; AssimpImporter::AssimpImporter() : m_importer(new Assimp::Importer) { m_importer->SetIOHandler(new AssimpIOSystem); m_importer->SetPropertyInteger(AI_CONFIG_PP_SBP_REMOVE, aiPrimitiveType_LINE | aiPrimitiveType_POINT); } bool AssimpImporter::load(const QString &filename) { uint flags = aiProcess_Triangulate | aiProcess_SortByPType | aiProcess_JoinIdenticalVertices | aiProcess_GenSmoothNormals | aiProcess_GenUVCoords | aiProcess_FlipUVs | aiProcess_FindDegenerates; if (opts.genTangents) flags |= aiProcess_CalcTangentSpace; const aiScene *scene = m_importer->ReadFile(filename.toUtf8().constData(), flags); if (!scene) return false; if (opts.showLog) { qDebug().noquote() << filename << scene->mNumMeshes << "meshes," << scene->mNumMaterials << "materials," << scene->mNumTextures << "embedded textures," << scene->mNumCameras << "cameras," << scene->mNumLights << "lights," << scene->mNumAnimations << "animations"; qDebug() << "Scene:"; printNodes(scene->mRootNode); } buildBuffer(); parseEmbeddedTextures(); parseMaterials(); parseCameras(); parseScene(); parseAnimations(); return true; } void AssimpImporter::printNodes(const aiNode *node, int level) { qDebug().noquote() << QString().fill('-', level * 4) << ai2qt(node->mName) << node->mNumMeshes << "mesh refs"; for (uint i = 0; i < node->mNumChildren; ++i) printNodes(node->mChildren[i], level + 1); } template void copyIndexBuf(T *dst, const aiMesh *src) { for (uint j = 0; j < src->mNumFaces; ++j) { const aiFace *f = &src->mFaces[j]; if (f->mNumIndices != 3) qFatal("Face %d is not a triangle (index count %d instead of 3)", j, f->mNumIndices); *dst++ = f->mIndices[0]; *dst++ = f->mIndices[1]; *dst++ = f->mIndices[2]; } } static QString newBufferViewName() { static int cnt = 0; return QString(QStringLiteral("bufferView_%1")).arg(++cnt); } static QString newAccessorName() { static int cnt = 0; return QString(QStringLiteral("accessor_%1")).arg(++cnt); } static QString newMeshName() { static int cnt = 0; return QString(QStringLiteral("mesh_%1")).arg(++cnt); } static QString newMaterialName() { static int cnt = 0; return QString(QStringLiteral("material_%1")).arg(++cnt); } static QString newTechniqueName() { static int cnt = 0; return QString(QStringLiteral("technique_%1")).arg(++cnt); } static QString newTextureName() { static int cnt = 0; return QString(QStringLiteral("texture_%1")).arg(++cnt); } static QString newImageName() { static int cnt = 0; return QString(QStringLiteral("image_%1")).arg(++cnt); } static QString newShaderName() { static int cnt = 0; return QString(QStringLiteral("shader_%1")).arg(++cnt); } static QString newProgramName() { static int cnt = 0; return QString(QStringLiteral("program_%1")).arg(++cnt); } static QString newNodeName() { static int cnt = 0; return QString(QStringLiteral("node_%1")).arg(++cnt); } static QString newAnimationName() { static int cnt = 0; return QString(QStringLiteral("animation_%1")).arg(++cnt); } template void calcBB(QVector &minVal, QVector &maxVal, T *data, int vertexCount, int compCount) { minVal.resize(compCount); maxVal.resize(compCount); for (int i = 0; i < vertexCount; ++i) { for (int j = 0; j < compCount; ++j) { if (i == 0) { minVal[j] = maxVal[j] = data[i][j]; } else { if (data[i][j] < minVal[j]) minVal[j] = data[i][j]; if (data[i][j] > maxVal[j]) maxVal[j] = data[i][j]; } } } } // One buffer per importer (scene). // Two buffer views (array, index) + three or more accessors per mesh. void AssimpImporter::buildBuffer() { m_buffer.clear(); m_meshInfo.clear(); if (opts.showLog) qDebug() << "Meshes:"; const aiScene *sc = scene(); for (uint i = 0; i < sc->mNumMeshes; ++i) { aiMesh *m = sc->mMeshes[i]; MeshInfo meshInfo; meshInfo.originalName = ai2qt(m->mName); meshInfo.name = newMeshName(); meshInfo.materialIndex = m->mMaterialIndex; aiVector3D *vertices = m->mVertices; aiVector3D *normals = m->mNormals; aiVector3D *textureCoords = m->mTextureCoords[0]; aiColor4D *colors = m->mColors[0]; aiVector3D *tangents = m->mTangents; if (opts.scale != 1) { for (uint j = 0; j < m->mNumVertices; ++j) { vertices[j].x *= opts.scale; vertices[j].y *= opts.scale; vertices[j].z *= opts.scale; } } // Vertex (3), Normal (3), Coord? (2), Color? (4), Tangent? (3) uint stride = 3 + 3 + (textureCoords ? 2 : 0) + (colors ? 4 : 0) + (tangents ? 3 : 0); QByteArray vertexBuf; vertexBuf.resize(stride * m->mNumVertices * sizeof(float)); float *p = reinterpret_cast(vertexBuf.data()); if (opts.interleave) { for (uint j = 0; j < m->mNumVertices; ++j) { // Vertex *p++ = vertices[j].x; *p++ = vertices[j].y; *p++ = vertices[j].z; // Normal *p++ = normals[j].x; *p++ = normals[j].y; *p++ = normals[j].z; // Coord if (textureCoords) { *p++ = textureCoords[j].x; *p++ = textureCoords[j].y; } // Color if (colors) { *p++ = colors[j].r; *p++ = colors[j].g; *p++ = colors[j].b; *p++ = colors[j].a; } // Tangent if (tangents) { *p++ = tangents[j].x; *p++ = tangents[j].y; *p++ = tangents[j].z; } } } else { // Vertex for (uint j = 0; j < m->mNumVertices; ++j) { *p++ = vertices[j].x; *p++ = vertices[j].y; *p++ = vertices[j].z; } // Normal for (uint j = 0; j < m->mNumVertices; ++j) { *p++ = normals[j].x; *p++ = normals[j].y; *p++ = normals[j].z; } // Coord if (textureCoords) { for (uint j = 0; j < m->mNumVertices; ++j) { *p++ = textureCoords[j].x; *p++ = textureCoords[j].y; } } // Color if (colors) { for (uint j = 0; j < m->mNumVertices; ++j) { *p++ = colors[j].r; *p++ = colors[j].g; *p++ = colors[j].b; *p++ = colors[j].a; } } // Tangent if (tangents) { for (uint j = 0; j < m->mNumVertices; ++j) { *p++ = tangents[j].x; *p++ = tangents[j].y; *p++ = tangents[j].z; } } } MeshInfo::BufferView vertexBufView; vertexBufView.name = newBufferViewName(); vertexBufView.length = vertexBuf.size(); vertexBufView.offset = m_buffer.size(); vertexBufView.componentType = GLT_FLOAT; vertexBufView.target = GLT_ARRAY_BUFFER; meshInfo.views.append(vertexBufView); QByteArray indexBuf; uint indexCount = m->mNumFaces * 3; if (indexCount >= USHRT_MAX) { indexBuf.resize(indexCount * sizeof(quint32)); quint32 *p = reinterpret_cast(indexBuf.data()); copyIndexBuf(p, m); } else { indexBuf.resize(indexCount * sizeof(quint16)); quint16 *p = reinterpret_cast(indexBuf.data()); copyIndexBuf(p, m); } MeshInfo::BufferView indexBufView; indexBufView.name = newBufferViewName(); indexBufView.length = indexBuf.size(); indexBufView.offset = vertexBufView.offset + vertexBufView.length; indexBufView.componentType = indexCount >= USHRT_MAX ? GLT_UNSIGNED_INT : GLT_UNSIGNED_SHORT; indexBufView.target = GLT_ELEMENT_ARRAY_BUFFER; meshInfo.views.append(indexBufView); MeshInfo::Accessor acc; uint startOffset = 0; // Vertex acc.name = newAccessorName(); acc.usage = QStringLiteral("POSITION"); acc.bufferView = vertexBufView.name; acc.offset = 0; acc.stride = opts.interleave ? stride * sizeof(float) : 3 * sizeof(float); acc.count = m->mNumVertices; acc.componentType = vertexBufView.componentType; acc.type = QStringLiteral("VEC3"); calcBB(acc.minVal, acc.maxVal, vertices, m->mNumVertices, 3); meshInfo.accessors.append(acc); startOffset += opts.interleave ? 3 : 3 * m->mNumVertices; // Normal acc.name = newAccessorName(); acc.usage = QStringLiteral("NORMAL"); acc.offset = startOffset * sizeof(float); if (!opts.interleave) acc.stride = 3 * sizeof(float); calcBB(acc.minVal, acc.maxVal, normals, m->mNumVertices, 3); meshInfo.accessors.append(acc); startOffset += opts.interleave ? 3 : 3 * m->mNumVertices; // Coord if (textureCoords) { acc.name = newAccessorName(); acc.usage = QStringLiteral("TEXCOORD_0"); acc.offset = startOffset * sizeof(float); if (!opts.interleave) acc.stride = 2 * sizeof(float); acc.type = QStringLiteral("VEC2"); calcBB(acc.minVal, acc.maxVal, textureCoords, m->mNumVertices, 2); meshInfo.accessors.append(acc); startOffset += opts.interleave ? 2 : 2 * m->mNumVertices; } // Color if (colors) { acc.name = newAccessorName(); acc.usage = QStringLiteral("COLOR"); acc.offset = startOffset * sizeof(float); if (!opts.interleave) acc.stride = 4 * sizeof(float); acc.type = QStringLiteral("VEC4"); calcBB(acc.minVal, acc.maxVal, colors, m->mNumVertices, 4); meshInfo.accessors.append(acc); startOffset += opts.interleave ? 4 : 4 * m->mNumVertices; } // Tangent if (tangents) { acc.name = newAccessorName(); acc.usage = QStringLiteral("TANGENT"); acc.offset = startOffset * sizeof(float); if (!opts.interleave) acc.stride = 3 * sizeof(float); acc.type = QStringLiteral("VEC3"); calcBB(acc.minVal, acc.maxVal, tangents, m->mNumVertices, 3); meshInfo.accessors.append(acc); startOffset += opts.interleave ? 3 : 3 * m->mNumVertices; } // Index acc.name = newAccessorName(); acc.usage = QStringLiteral("INDEX"); acc.bufferView = indexBufView.name; acc.offset = 0; acc.stride = 0; acc.count = indexCount; acc.componentType = indexBufView.componentType; acc.type = QStringLiteral("SCALAR"); acc.minVal = acc.maxVal = QVector(); meshInfo.accessors.append(acc); if (opts.showLog) { qDebug().noquote() << "#" << i << "(" << meshInfo.name << "/" << meshInfo.originalName << ")" << m->mNumVertices << "vertices," << m->mNumFaces << "faces," << stride << "bytes per vertex," << vertexBuf.size() << "vertex bytes," << indexBuf.size() << "index bytes"; if (opts.scale != 1) qDebug() << " scaled by" << opts.scale; if (!opts.interleave) qDebug() << " non-interleaved layout"; QStringList sl; for (const MeshInfo::BufferView &bv : qAsConst(meshInfo.views)) sl << bv.name; qDebug() << " buffer views:" << sl; sl.clear(); for (const MeshInfo::Accessor &acc : qAsConst(meshInfo.accessors)) sl << acc.name; qDebug() << " accessors:" << sl; qDebug() << " material: #" << meshInfo.materialIndex; } m_buffer.append(vertexBuf); m_buffer.append(indexBuf); m_meshInfo.insert(i, meshInfo); } if (opts.showLog) qDebug().noquote() << "Total buffer size" << m_buffer.size(); } void AssimpImporter::parseEmbeddedTextures() { #ifdef HAS_QIMAGE m_embeddedTextures.clear(); const aiScene *sc = scene(); if (opts.showLog && sc->mNumTextures) qDebug() << "Embedded textures:"; for (uint i = 0; i < sc->mNumTextures; ++i) { aiTexture *t = sc->mTextures[i]; QImage img; if (t->mHeight == 0) { img = QImage::fromData(reinterpret_cast(t->pcData), t->mWidth); } else { uint sz = t->mWidth * t->mHeight; QByteArray data; data.resize(sz * 4); uchar *p = reinterpret_cast(data.data()); for (uint j = 0; j < sz; ++j) { *p++ = t->pcData[j].r; *p++ = t->pcData[j].g; *p++ = t->pcData[j].b; *p++ = t->pcData[j].a; } img = QImage(reinterpret_cast(data.constData()), t->mWidth, t->mHeight, QImage::Format_RGBA8888); img.detach(); } QString name; static int imgCnt = 0; name = QString(QStringLiteral("texture_%1.png")).arg(++imgCnt); QString embeddedTextureRef = QStringLiteral("*") + QString::number(i); // see AI_MAKE_EMBEDDED_TEXNAME m_embeddedTextures.insert(embeddedTextureRef, EmbeddedTextureInfo(name, img)); if (opts.showLog) qDebug().noquote() << "#" << i << name << img; } #else if (scene()->mNumTextures) qWarning() << "WARNING: No image support, ignoring" << scene()->mNumTextures << "embedded textures"; #endif } void AssimpImporter::parseMaterials() { m_materialInfo.clear(); m_externalTextures.clear(); if (opts.showLog) qDebug() << "Materials:"; const aiScene *sc = scene(); for (uint i = 0; i < sc->mNumMaterials; ++i) { const aiMaterial *mat = sc->mMaterials[i]; MaterialInfo matInfo; matInfo.name = newMaterialName(); aiString s; if (mat->Get(AI_MATKEY_NAME, s) == aiReturn_SUCCESS) matInfo.originalName = ai2qt(s); aiColor4D color; if (mat->Get(AI_MATKEY_COLOR_DIFFUSE, color) == aiReturn_SUCCESS) matInfo.m_colors.insert("diffuse", QVector() << color.r << color.g << color.b << color.a); if (mat->Get(AI_MATKEY_COLOR_SPECULAR, color) == aiReturn_SUCCESS) matInfo.m_colors.insert("specular", QVector() << color.r << color.g << color.b); if (mat->Get(AI_MATKEY_COLOR_AMBIENT, color) == aiReturn_SUCCESS) matInfo.m_colors.insert("ambient", QVector() << color.r << color.g << color.b); float f; if (mat->Get(AI_MATKEY_SHININESS, f) == aiReturn_SUCCESS) matInfo.m_values.insert("shininess", f); if (mat->GetTexture(aiTextureType_DIFFUSE, 0, &s) == aiReturn_SUCCESS) matInfo.m_textures.insert("diffuse", ai2qt(s)); if (mat->GetTexture(aiTextureType_SPECULAR, 0, &s) == aiReturn_SUCCESS) matInfo.m_textures.insert("specular", ai2qt(s)); if (mat->GetTexture(aiTextureType_NORMALS, 0, &s) == aiReturn_SUCCESS) matInfo.m_textures.insert("normal", ai2qt(s)); QHash::iterator texIt = matInfo.m_textures.begin(); while (texIt != matInfo.m_textures.end()) { // Map embedded texture references to real files. if (texIt->startsWith('*')) *texIt = m_embeddedTextures[*texIt].name; else m_externalTextures.insert(*texIt); ++texIt; } m_materialInfo.insert(i, matInfo); if (opts.showLog) { qDebug().noquote() << "#" << i << "(" << matInfo.name << "/" << matInfo.originalName << ")"; qDebug() << " colors:" << matInfo.m_colors; qDebug() << " values:" << matInfo.m_values; qDebug() << " textures:" << matInfo.m_textures; } } } void AssimpImporter::parseCameras() { m_cameraInfo.clear(); if (opts.showLog) qDebug() << "Cameras:"; const aiScene *sc = scene(); for (uint i = 0; i < sc->mNumCameras; ++i) { const aiCamera *cam = sc->mCameras[i]; QString name = ai2qt(cam->mName); CameraInfo c; c.name = name + QStringLiteral("_cam"); c.aspectRatio = qFuzzyIsNull(cam->mAspect) ? 1.5f : cam->mAspect; c.yfov = cam->mHorizontalFOV; if (c.yfov < (M_PI / 10.0)) // this can't be right (probably orthographic source camera) c.yfov = float(M_PI / 4.0); c.znear = cam->mClipPlaneNear; c.zfar = cam->mClipPlaneFar; // Collada / glTF cameras point in -Z by default, the rest is in the // node matrix, no separate look-at params given here. m_cameraInfo.insert(name, c); if (opts.showLog) qDebug().noquote() << "#" << i << "(" << name << ")" << c.aspectRatio << c.yfov << c.znear << c.zfar; } } void AssimpImporter::parseNode(Importer::Node *dst, const aiNode *src) { dst->name = ai2qt(src->mName); dst->uniqueName = newNodeName(); for (uint j = 0; j < src->mNumChildren; ++j) { Node *c = new Node; parseNode(c, src->mChildren[j]); dst->children << c; } dst->transformation = ai2qt(src->mTransformation); for (uint j = 0; j < src->mNumMeshes; ++j) dst->meshes << src->mMeshes[j]; } void AssimpImporter::parseScene() { delNode(m_rootNode); const aiScene *sc = scene(); m_rootNode = new Node; parseNode(m_rootNode, sc->mRootNode); } void AssimpImporter::addKeyFrame(QVector &keyFrames, float t, aiVector3D *vt, aiQuaternion *vr, aiVector3D *vs) { KeyFrame kf; int idx = -1; for (int i = 0; i < keyFrames.count(); ++i) { if (qFuzzyCompare(keyFrames[i].t, t)) { kf = keyFrames[i]; idx = i; break; } } kf.t = t; if (vt) { kf.transValid = true; kf.trans = QVector() << vt->x << vt->y << vt->z; } if (vr) { kf.rotValid = true; kf.rot = QVector() << vr->w << vr->x << vr->y << vr->z; } if (vs) { kf.scaleValid = true; kf.scale = QVector() << vs->x << vs->y << vs->z; } if (idx >= 0) keyFrames[idx] = kf; else keyFrames.append(kf); } void AssimpImporter::parseAnimations() { const aiScene *sc = scene(); if (opts.showLog && sc->mNumAnimations) qDebug() << "Animations:"; for (uint i = 0; i < sc->mNumAnimations; ++i) { const aiAnimation *anim = sc->mAnimations[i]; // Only care about node animations. for (uint j = 0; j < anim->mNumChannels; ++j) { const aiNodeAnim *a = anim->mChannels[j]; AnimationInfo animInfo; QVector keyFrames; if (opts.showLog) qDebug().noquote() << ai2qt(anim->mName) << "->" << ai2qt(a->mNodeName); // Target values in the keyframes are local absolute (relative to parent, like node.matrix). for (uint kf = 0; kf < a->mNumPositionKeys; ++kf) { float t = float(a->mPositionKeys[kf].mTime); aiVector3D v = a->mPositionKeys[kf].mValue; animInfo.hasTranslation = true; addKeyFrame(keyFrames, t, &v, nullptr, nullptr); } for (uint kf = 0; kf < a->mNumRotationKeys; ++kf) { float t = float(a->mRotationKeys[kf].mTime); aiQuaternion v = a->mRotationKeys[kf].mValue; animInfo.hasRotation = true; addKeyFrame(keyFrames, t, nullptr, &v, nullptr); } for (uint kf = 0; kf < a->mNumScalingKeys; ++kf) { float t = float(a->mScalingKeys[kf].mTime); aiVector3D v = a->mScalingKeys[kf].mValue; animInfo.hasScale = true; addKeyFrame(keyFrames, t, nullptr, nullptr, &v); } // Here we should ideally get rid of non-animated properties (that // just set the t-r-s value from node.matrix in every frame) but // let's leave that as a future exercise. if (!keyFrames.isEmpty()) { animInfo.name = ai2qt(anim->mName); QString nodeName = ai2qt(a->mNodeName); // have to map to our generated, unique node names const Node *targetNode = findNode(m_rootNode, nodeName); if (targetNode) animInfo.targetNode = targetNode->uniqueName; else qWarning().noquote() << "ERROR: Cannot find target node" << nodeName << "for animation" << animInfo.name; animInfo.keyFrames = keyFrames; m_animations << animInfo; if (opts.showLog) { for (const KeyFrame &kf : qAsConst(keyFrames)) { QString msg; QTextStream s(&msg); s << " @ " << kf.t; if (kf.transValid) s << " T=(" << kf.trans[0] << ", " << kf.trans[1] << ", " << kf.trans[2] << ")"; if (kf.rotValid) s << " R=(w=" << kf.rot[0] << ", " << kf.rot[1] << ", " << kf.rot[2] << ", " << kf.rot[3] << ")"; if (kf.scaleValid) s << " S=(" << kf.scale[0] << ", " << kf.scale[1] << ", " << kf.scale[2] << ")"; qDebug().noquote() << msg; } } } } } } const aiScene *AssimpImporter::scene() const { return m_importer->GetScene(); } class Exporter { public: Exporter(Importer *importer) : m_importer(importer) { } virtual ~Exporter() { } virtual void save(const QString &inputFilename) = 0; protected: bool nodeIsUseful(const Importer::Node *n) const; void copyExternalTextures(const QString &inputFilename); void exportEmbeddedTextures(); void compressTextures(); Importer *m_importer; QSet m_files; QHash m_compressedTextures; }; bool Exporter::nodeIsUseful(const Importer::Node *n) const { if (!n->meshes.isEmpty() || m_importer->cameraInfo().contains(n->name)) return true; for (const Importer::Node *c : n->children) { if (nodeIsUseful(c)) return true; } return false; } void Exporter::copyExternalTextures(const QString &inputFilename) { const auto textureFilenames = m_importer->externalTextures(); for (const QString &textureFilename : textureFilenames) { const QString dst = opts.outDir + textureFilename; m_files.insert(QFileInfo(dst).fileName()); // External textures need copying only when output dir was specified. if (!opts.outDir.isEmpty()) { const QString src = QFileInfo(inputFilename).path() + QStringLiteral("/") + textureFilename; if (QFileInfo(src).absolutePath() != QFileInfo(dst).absolutePath()) { if (opts.showLog) qDebug().noquote() << "Copying" << src << "to" << dst; QFile(src).copy(dst); } } } } void Exporter::exportEmbeddedTextures() { #ifdef HAS_QIMAGE const auto embeddedTextures = m_importer->embeddedTextures(); for (const Importer::EmbeddedTextureInfo &embTex : embeddedTextures) { QString fn = opts.outDir + embTex.name; m_files.insert(QFileInfo(fn).fileName()); if (opts.showLog) qDebug().noquote() << "Writing" << fn; embTex.image.save(fn); } #endif } void Exporter::compressTextures() { if (opts.texComp != Options::ETC1) return; const auto textureFilenames = m_importer->externalTextures(); const auto embeddedTextures = m_importer->embeddedTextures(); QStringList imageList; imageList.reserve(textureFilenames.size() + embeddedTextures.size()); for (const QString &textureFilename : textureFilenames) imageList << opts.outDir + textureFilename; for (const Importer::EmbeddedTextureInfo &embTex : embeddedTextures) imageList << opts.outDir + embTex.name; for (const QString &filename : qAsConst(imageList)) { if (QFileInfo(filename).suffix().toLower() != QStringLiteral("png")) continue; QByteArray cmd = QByteArrayLiteral("etc1tool "); cmd += filename.toUtf8(); qDebug().noquote() << "Invoking" << cmd; // No QProcess in bootstrap if (system(cmd.constData()) == -1) { qWarning() << "ERROR: Failed to launch etc1tool"; } else { QString src = QFileInfo(filename).fileName(); QString dst = QFileInfo(src).baseName() + QStringLiteral(".pkm"); m_compressedTextures.insert(src, dst); m_files.remove(src); m_files.insert(dst); } } } class GltfExporter : public Exporter { public: GltfExporter(Importer *importer); void save(const QString &inputFilename) override; private: struct ProgramInfo { struct Param { Param() : type(0) { } Param(QString name, QString nameInShader, QString semantic, uint type) : name(name), nameInShader(nameInShader), semantic(semantic), type(type) { } QString name; QString nameInShader; QString semantic; uint type; }; QString commonTechniqueName; QString vertShader; QString fragShader; QVector attributes; QVector uniforms; }; friend class QTypeInfo; friend class QTypeInfo; void writeShader(const QString &src, const QString &dst, const QVector > &substTab); QString exportNode(const Importer::Node *n, QJsonObject &nodes); void exportMaterials(QJsonObject &materials, QHash *textureNameMap); void exportParameter(QJsonObject &dst, const QVector ¶ms); void exportTechniques(QJsonObject &obj, const QString &basename); void exportAnimations(QJsonObject &obj, QVector &bufList, QVector &bvList, QVector &accList); void initShaderInfo(); ProgramInfo *chooseProgram(uint materialIndex); QJsonObject m_obj; QJsonDocument m_doc; QVector m_progs; struct TechniqueInfo { TechniqueInfo() : opaque(true), prog(nullptr) { } TechniqueInfo(const QString &name, bool opaque, ProgramInfo *prog) : name(name) , opaque(opaque) , prog(prog) { coreName = name + QStringLiteral("_core"); gl2Name = name + QStringLiteral("_gl2"); } QString name; QString coreName; QString gl2Name; bool opaque; ProgramInfo *prog; }; friend class QTypeInfo; QVector m_techniques; QSet m_usedPrograms; QVector > m_subst_es2; QVector > m_subst_core; QHash m_imageHasAlpha; }; QT_BEGIN_NAMESPACE Q_DECLARE_TYPEINFO(GltfExporter::ProgramInfo, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(GltfExporter::ProgramInfo::Param, Q_MOVABLE_TYPE); Q_DECLARE_TYPEINFO(GltfExporter::TechniqueInfo, Q_MOVABLE_TYPE); QT_END_NAMESPACE GltfExporter::GltfExporter(Importer *importer) : Exporter(importer) { initShaderInfo(); } struct Shader { const char *name; const char *text; } shaders[] = { { "color.vert", "$VERSION\n" "$ATTRIBUTE vec3 vertexPosition;\n" "$ATTRIBUTE vec3 vertexNormal;\n" "$VVARYING vec3 vPosition;\n" "$VVARYING vec3 vNormal;\n" "uniform mat4 projection;\n" "uniform mat4 modelView;\n" "uniform mat3 modelViewNormal;\n" "void main()\n" "{\n" " vNormal = normalize( modelViewNormal * vertexNormal );\n" " vPosition = vec3( modelView * vec4( vertexPosition, 1.0 ) );\n" " gl_Position = projection * modelView * vec4( vertexPosition, 1.0 );\n" "}\n" }, { "color.frag", "$VERSION\n" "uniform $HIGHP vec4 lightPosition;\n" "uniform $HIGHP vec3 lightIntensity;\n" "uniform $HIGHP vec3 ka;\n" "uniform $HIGHP vec4 kd;\n" "uniform $HIGHP vec3 ks;\n" "uniform $HIGHP float shininess;\n" "$FVARYING $HIGHP vec3 vPosition;\n" "$FVARYING $HIGHP vec3 vNormal;\n" "$DECL_FRAGCOLOR\n" "$HIGHP vec3 adsModel( const $HIGHP vec3 pos, const $HIGHP vec3 n )\n" "{\n" " $HIGHP vec3 s = normalize( vec3( lightPosition ) - pos );\n" " $HIGHP vec3 v = normalize( -pos );\n" " $HIGHP vec3 r = reflect( -s, n );\n" " $HIGHP float diffuse = max( dot( s, n ), 0.0 );\n" " $HIGHP float specular = 0.0;\n" " if ( dot( s, n ) > 0.0 )\n" " specular = pow( max( dot( r, v ), 0.0 ), shininess );\n" " return lightIntensity * ( ka + kd.rgb * diffuse + ks * specular );\n" "}\n" "void main()\n" "{\n" " $FRAGCOLOR = vec4( adsModel( vPosition, normalize( vNormal ) ) * kd.a, kd.a );\n" "}\n" }, { "diffusemap.vert", "$VERSION\n" "$ATTRIBUTE vec3 vertexPosition;\n" "$ATTRIBUTE vec3 vertexNormal;\n" "$ATTRIBUTE vec2 vertexTexCoord;\n" "$VVARYING vec3 vPosition;\n" "$VVARYING vec3 vNormal;\n" "$VVARYING vec2 vTexCoord;\n" "uniform mat4 projection;\n" "uniform mat4 modelView;\n" "uniform mat3 modelViewNormal;\n" "void main()\n" "{\n" " vTexCoord = vertexTexCoord;\n" " vNormal = normalize( modelViewNormal * vertexNormal );\n" " vPosition = vec3( modelView * vec4( vertexPosition, 1.0 ) );\n" " gl_Position = projection * modelView * vec4( vertexPosition, 1.0 );\n" "}\n" }, { "diffusemap.frag", "$VERSION\n" "uniform $HIGHP vec4 lightPosition;\n" "uniform $HIGHP vec3 lightIntensity;\n" "uniform $HIGHP vec3 ka;\n" "uniform $HIGHP vec3 ks;\n" "uniform $HIGHP float shininess;\n" "uniform sampler2D diffuseTexture;\n" "$FVARYING $HIGHP vec3 vPosition;\n" "$FVARYING $HIGHP vec3 vNormal;\n" "$FVARYING $HIGHP vec2 vTexCoord;\n" "$DECL_FRAGCOLOR\n" "$HIGHP vec4 adsModel( const $HIGHP vec3 pos, const $HIGHP vec3 n )\n" "{\n" " $HIGHP vec3 s = normalize( vec3( lightPosition ) - pos );\n" " $HIGHP vec3 v = normalize( -pos );\n" " $HIGHP vec3 r = reflect( -s, n );\n" " $HIGHP float diffuse = max( dot( s, n ), 0.0 );\n" " $HIGHP float specular = 0.0;\n" " if ( dot( s, n ) > 0.0 )\n" " specular = pow( max( dot( r, v ), 0.0 ), shininess );\n" " $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, vTexCoord );\n" " return vec4( lightIntensity * ( ka + kd.rgb * diffuse + ks * specular ) * kd.a, kd.a );\n" "}\n" "void main()\n" "{\n" " $FRAGCOLOR = adsModel( vPosition, normalize( vNormal ) );\n" "}\n" }, { "diffusespecularmap.frag", "$VERSION\n" "uniform $HIGHP vec4 lightPosition;\n" "uniform $HIGHP vec3 lightIntensity;\n" "uniform $HIGHP vec3 ka;\n" "uniform $HIGHP float shininess;\n" "uniform sampler2D diffuseTexture;\n" "uniform sampler2D specularTexture;\n" "$FVARYING $HIGHP vec3 vPosition;\n" "$FVARYING $HIGHP vec3 vNormal;\n" "$FVARYING $HIGHP vec2 vTexCoord;\n" "$DECL_FRAGCOLOR\n" "$HIGHP vec4 adsModel( const in $HIGHP vec3 pos, const in $HIGHP vec3 n )\n" "{\n" " $HIGHP vec3 s = normalize( vec3( lightPosition ) - pos );\n" " $HIGHP vec3 v = normalize( -pos );\n" " $HIGHP vec3 r = reflect( -s, n );\n" " $HIGHP float diffuse = max( dot( s, n ), 0.0 );\n" " $HIGHP float specular = 0.0;\n" " if ( dot( s, n ) > 0.0 )\n" " specular = ( shininess / ( 8.0 * 3.14 ) ) * pow( max( dot( r, v ), 0.0 ), shininess );\n" " $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, vTexCoord );\n" " $HIGHP vec3 ks = $TEXTURE2D( specularTexture, vTexCoord );\n" " return vec4( lightIntensity * ( ka + kd.rgb * diffuse + ks * specular ) * kd.a, kd.a );\n" "}\n" "void main()\n" "{\n" " $FRAGCOLOR = vec4( adsModel( vPosition, normalize( vNormal ) ), 1.0 );\n" "}\n" }, { "normaldiffusemap.vert", "$VERSION\n" "$ATTRIBUTE vec3 vertexPosition;\n" "$ATTRIBUTE vec3 vertexNormal;\n" "$ATTRIBUTE vec2 vertexTexCoord;\n" "$ATTRIBUTE vec4 vertexTangent;\n" "$VVARYING vec3 lightDir;\n" "$VVARYING vec3 viewDir;\n" "$VVARYING vec2 texCoord;\n" "uniform mat4 projection;\n" "uniform mat4 modelView;\n" "uniform mat3 modelViewNormal;\n" "uniform vec4 lightPosition;\n" "void main()\n" "{\n" " texCoord = vertexTexCoord;\n" " vec3 normal = normalize( modelViewNormal * vertexNormal );\n" " vec3 tangent = normalize( modelViewNormal * vertexTangent.xyz );\n" " vec3 position = vec3( modelView * vec4( vertexPosition, 1.0 ) );\n" " vec3 binormal = normalize( cross( normal, tangent ) );\n" " mat3 tangentMatrix = mat3 (\n" " tangent.x, binormal.x, normal.x,\n" " tangent.y, binormal.y, normal.y,\n" " tangent.z, binormal.z, normal.z );\n" " vec3 s = vec3( lightPosition ) - position;\n" " lightDir = normalize( tangentMatrix * s );\n" " vec3 v = -position;\n" " viewDir = normalize( tangentMatrix * v );\n" " gl_Position = projection * modelView * vec4( vertexPosition, 1.0 );\n" "}\n" }, { "normaldiffusemap.frag", "$VERSION\n" "uniform $HIGHP vec3 lightIntensity;\n" "uniform $HIGHP vec3 ka;\n" "uniform $HIGHP vec3 ks;\n" "uniform $HIGHP float shininess;\n" "uniform sampler2D diffuseTexture;\n" "uniform sampler2D normalTexture;\n" "$FVARYING $HIGHP vec3 lightDir;\n" "$FVARYING $HIGHP vec3 viewDir;\n" "$FVARYING $HIGHP vec2 texCoord;\n" "$DECL_FRAGCOLOR\n" "$HIGHP vec3 adsModel( const $HIGHP vec3 norm, const $HIGHP vec3 diffuseReflect)\n" "{\n" " $HIGHP vec3 r = reflect( -lightDir, norm );\n" " $HIGHP vec3 ambient = lightIntensity * ka;\n" " $HIGHP float sDotN = max( dot( lightDir, norm ), 0.0 );\n" " $HIGHP vec3 diffuse = lightIntensity * diffuseReflect * sDotN;\n" " $HIGHP vec3 ambientAndDiff = ambient + diffuse;\n" " $HIGHP vec3 spec = vec3( 0.0 );\n" " if ( sDotN > 0.0 )\n" " spec = lightIntensity * ks * pow( max( dot( r, viewDir ), 0.0 ), shininess );\n" " return ambientAndDiff + spec;\n" "}\n" "void main()\n" "{\n" " $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, texCoord );\n" " $HIGHP vec4 normal = 2.0 * $TEXTURE2D( normalTexture, texCoord ) - vec4( 1.0 );\n" " $FRAGCOLOR = vec4( adsModel( normalize( normal.xyz ), kd.rgb) * kd.a, kd.a );\n" "}\n" }, { "normaldiffusespecularmap.frag", "$VERSION\n" "uniform $HIGHP vec3 lightIntensity;\n" "uniform $HIGHP vec3 ka;\n" "uniform $HIGHP float shininess;\n" "uniform sampler2D diffuseTexture;\n" "uniform sampler2D specularTexture;\n" "uniform sampler2D normalTexture;\n" "$FVARYING $HIGHP vec3 lightDir;\n" "$FVARYING $HIGHP vec3 viewDir;\n" "$FVARYING $HIGHP vec2 texCoord;\n" "$DECL_FRAGCOLOR\n" "$HIGHP vec3 adsModel( const $HIGHP vec3 norm, const $HIGHP vec3 diffuseReflect, const $HIGHP vec3 specular )\n" "{\n" " $HIGHP vec3 r = reflect( -lightDir, norm );\n" " $HIGHP vec3 ambient = lightIntensity * ka;\n" " $HIGHP float sDotN = max( dot( lightDir, norm ), 0.0 );\n" " $HIGHP vec3 diffuse = lightIntensity * diffuseReflect * sDotN;\n" " $HIGHP vec3 ambientAndDiff = ambient + diffuse;\n" " $HIGHP vec3 spec = vec3( 0.0 );\n" " if ( sDotN > 0.0 )\n" " spec = lightIntensity * ( shininess / ( 8.0 * 3.14 ) ) * pow( max( dot( r, viewDir ), 0.0 ), shininess );\n" " return (ambientAndDiff + spec * specular.rgb);\n" "}\n" "void main()\n" "{\n" " $HIGHP vec4 kd = $TEXTURE2D( diffuseTexture, texCoord );\n" " $HIGHP vec3 ks = $TEXTURE2D( specularTexture, texCoord );\n" " $HIGHP vec4 normal = 2.0 * $TEXTURE2D( normalTexture, texCoord ) - vec4( 1.0 );\n" " $FRAGCOLOR = vec4( adsModel( normalize( normal.xyz ), kd.rgb, ks ) * kd.a, kd.a );\n" "}\n" } }; void GltfExporter::initShaderInfo() { ProgramInfo p; p = ProgramInfo(); p.commonTechniqueName = "PHONG"; // diffuse RGBA, specular RGBA p.vertShader = "color.vert"; p.fragShader = "color.frag"; p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3); p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4); p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("diffuse", "kd", QString(), GLT_FLOAT_VEC4); p.uniforms << ProgramInfo::Param("specular", "ks", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT); m_progs << p; p = ProgramInfo(); p.commonTechniqueName = "PHONG"; // diffuse texture, specular RGBA p.vertShader = "diffusemap.vert"; p.fragShader = "diffusemap.frag"; p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2); p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3); p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4); p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("specular", "ks", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT); p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D); m_progs << p; p = ProgramInfo(); p.commonTechniqueName = "PHONG"; // diffuse texture, specular texture p.vertShader = "diffusemap.vert"; p.fragShader = "diffusespecularmap.frag"; p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2); p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3); p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4); p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT); p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D); p.uniforms << ProgramInfo::Param("specular", "specularTexture", QString(), GLT_SAMPLER_2D); m_progs << p; p = ProgramInfo(); p.commonTechniqueName = "PHONG"; // diffuse texture, specular RGBA, normalmap texture p.vertShader = "normaldiffusemap.vert"; p.fragShader = "normaldiffusemap.frag"; p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2); p.attributes << ProgramInfo::Param("tangent", "vertexTangent", "TANGENT", GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3); p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4); p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("specular", "ks", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT); p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D); p.uniforms << ProgramInfo::Param("normalmap", "normalTexture", QString(), GLT_SAMPLER_2D); m_progs << p; p = ProgramInfo(); p.commonTechniqueName = "PHONG"; // diffuse texture, specular texture, normalmap texture p.vertShader = "normaldiffusemap.vert"; p.fragShader = "normaldiffusespecularmap.frag"; p.attributes << ProgramInfo::Param("position", "vertexPosition", "POSITION", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("normal", "vertexNormal", "NORMAL", GLT_FLOAT_VEC3); p.attributes << ProgramInfo::Param("texcoord0", "vertexTexCoord", "TEXCOORD_0", GLT_FLOAT_VEC2); p.attributes << ProgramInfo::Param("tangent", "vertexTangent", "TANGENT", GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("projection", "projection", "PROJECTION", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("modelView", "modelView", "MODELVIEW", GLT_FLOAT_MAT4); p.uniforms << ProgramInfo::Param("normalMatrix", "modelViewNormal", "MODELVIEWINVERSETRANSPOSE", GLT_FLOAT_MAT3); p.uniforms << ProgramInfo::Param("lightPosition", "lightPosition", QString(), GLT_FLOAT_VEC4); p.uniforms << ProgramInfo::Param("lightIntensity", "lightIntensity", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("ambient", "ka", QString(), GLT_FLOAT_VEC3); p.uniforms << ProgramInfo::Param("shininess", "shininess", QString(), GLT_FLOAT); p.uniforms << ProgramInfo::Param("diffuse", "diffuseTexture", QString(), GLT_SAMPLER_2D); p.uniforms << ProgramInfo::Param("specular", "specularTexture", QString(), GLT_SAMPLER_2D); p.uniforms << ProgramInfo::Param("normalmap", "normalTexture", QString(), GLT_SAMPLER_2D); m_progs << p; m_subst_es2 << qMakePair(QByteArrayLiteral("$VERSION"), QByteArray()); m_subst_es2 << qMakePair(QByteArrayLiteral("$ATTRIBUTE"), QByteArrayLiteral("attribute")); m_subst_es2 << qMakePair(QByteArrayLiteral("$VVARYING"), QByteArrayLiteral("varying")); m_subst_es2 << qMakePair(QByteArrayLiteral("$FVARYING"), QByteArrayLiteral("varying")); m_subst_es2 << qMakePair(QByteArrayLiteral("$TEXTURE2D"), QByteArrayLiteral("texture2D")); m_subst_es2 << qMakePair(QByteArrayLiteral("$DECL_FRAGCOLOR"), QByteArray()); m_subst_es2 << qMakePair(QByteArrayLiteral("$FRAGCOLOR"), QByteArrayLiteral("gl_FragColor")); m_subst_es2 << qMakePair(QByteArrayLiteral("$HIGHP"), QByteArrayLiteral("highp")); m_subst_core << qMakePair(QByteArrayLiteral("$VERSION"), QByteArrayLiteral("#version 150 core")); m_subst_core << qMakePair(QByteArrayLiteral("$ATTRIBUTE"), QByteArrayLiteral("in")); m_subst_core << qMakePair(QByteArrayLiteral("$VVARYING"), QByteArrayLiteral("out")); m_subst_core << qMakePair(QByteArrayLiteral("$FVARYING"), QByteArrayLiteral("in")); m_subst_core << qMakePair(QByteArrayLiteral("$TEXTURE2D"), QByteArrayLiteral("texture")); m_subst_core << qMakePair(QByteArrayLiteral("$DECL_FRAGCOLOR"), QByteArrayLiteral("out vec4 fragColor;")); m_subst_core << qMakePair(QByteArrayLiteral("$FRAGCOLOR"), QByteArrayLiteral("fragColor")); m_subst_core << qMakePair(QByteArrayLiteral("$HIGHP "), QByteArray()); } GltfExporter::ProgramInfo *GltfExporter::chooseProgram(uint materialIndex) { Importer::MaterialInfo matInfo = m_importer->materialInfo(materialIndex); const bool hasNormalTexture = matInfo.m_textures.contains("normal"); const bool hasSpecularTexture = matInfo.m_textures.contains("specular"); const bool hasDiffuseTexture = matInfo.m_textures.contains("diffuse"); if (hasNormalTexture && !m_importer->allMeshesForMaterialHaveTangents(materialIndex)) qWarning() << "WARNING: Tangent vectors not exported while the material requires it. (hint: try -t)"; if (hasNormalTexture && hasSpecularTexture && hasDiffuseTexture) { if (opts.showLog) qDebug() << "Using program taking diffuse, specular, normal textures"; return &m_progs[4]; } if (hasNormalTexture && hasDiffuseTexture) { if (opts.showLog) qDebug() << "Using program taking diffuse, normal textures"; return &m_progs[3]; } if (hasSpecularTexture && hasDiffuseTexture) { if (opts.showLog) qDebug() << "Using program taking diffuse, specular textures"; return &m_progs[2]; } if (hasDiffuseTexture) { if (opts.showLog) qDebug() << "Using program taking diffuse texture"; return &m_progs[1]; } if (opts.showLog) qDebug() << "Using program without textures"; return &m_progs[0]; } QString GltfExporter::exportNode(const Importer::Node *n, QJsonObject &nodes) { QJsonObject node; node["name"] = n->name; QJsonArray children; for (const Importer::Node *c : n->children) { if (nodeIsUseful(c)) children << exportNode(c, nodes); } node["children"] = children; QJsonArray matrix; const float *mtxp = n->transformation.constData(); for (int j = 0; j < 16; ++j) matrix.append(*mtxp++); node["matrix"] = matrix; QJsonArray meshList; for (int j = 0; j < n->meshes.count(); ++j) meshList.append(m_importer->meshInfo(n->meshes[j]).name); if (!meshList.isEmpty()) { node["meshes"] = meshList; } else { QHash cam = m_importer->cameraInfo(); if (cam.contains(n->name)) node["camera"] = cam[n->name].name; } nodes[n->uniqueName] = node; return n->uniqueName; } static inline QJsonArray col2jsvec(const QVector &color, bool alpha = false) { QJsonArray arr; arr << color[0] << color[1] << color[2]; if (alpha) arr << color[3]; return arr; } static inline QJsonArray vec2jsvec(const QVector &v) { QJsonArray arr; for (int i = 0; i < v.count(); ++i) arr << v[i]; return arr; } static inline void promoteColorsToRGBA(QJsonObject *obj) { QJsonObject::iterator it = obj->begin(), itEnd = obj->end(); while (it != itEnd) { QJsonArray arr = it.value().toArray(); if (arr.count() == 3) { const QString key = it.key(); if (key == QStringLiteral("ambient") || key == QStringLiteral("diffuse") || key == QStringLiteral("specular")) { arr.append(1); *it = arr; } } ++it; } } void GltfExporter::exportMaterials(QJsonObject &materials, QHash *textureNameMap) { for (uint i = 0; i < m_importer->materialCount(); ++i) { Importer::MaterialInfo matInfo = m_importer->materialInfo(i); QJsonObject material; material["name"] = matInfo.originalName; bool opaque = true; QJsonObject vals; for (QHash::const_iterator it = matInfo.m_textures.constBegin(); it != matInfo.m_textures.constEnd(); ++it) { if (!textureNameMap->contains(it.value())) textureNameMap->insert(it.value(), newTextureName()); QByteArray key = it.key(); if (key == QByteArrayLiteral("normal")) // avoid clashing with the vertex normals key = QByteArrayLiteral("normalmap"); // alpha is supported for diffuse textures, but have to check the image data to decide if blending is needed if (key == QByteArrayLiteral("diffuse")) { QString imgFn = opts.outDir + it.value(); if (m_imageHasAlpha.contains(imgFn)) { if (m_imageHasAlpha[imgFn]) opaque = false; } else { #ifdef HAS_QIMAGE QImage img(imgFn); if (!img.isNull()) { if (img.hasAlphaChannel()) { for (int y = 0; opaque && y < img.height(); ++y) for (int x = 0; opaque && x < img.width(); ++x) if (qAlpha(img.pixel(x, y)) < 255) opaque = false; } m_imageHasAlpha[imgFn] = !opaque; } else { qWarning() << "WARNING: Cannot determine presence of alpha for" << imgFn; } #else qWarning() << "WARNING: No image support, assuming all textures are opaque"; #endif } } vals[key] = textureNameMap->value(it.value()); } for (QHash::const_iterator it = matInfo.m_values.constBegin(); it != matInfo.m_values.constEnd(); ++it) { if (vals.contains(it.key())) continue; vals[it.key()] = it.value(); } for (QHash >::const_iterator it = matInfo.m_colors.constBegin(); it != matInfo.m_colors.constEnd(); ++it) { if (vals.contains(it.key())) continue; // alpha is supported for the diffuse color. < 1 will enable blending. const bool alpha = it.key() == QStringLiteral("diffuse"); if (alpha && it.value()[3] < 1.0f) opaque = false; vals[it.key()] = col2jsvec(it.value(), alpha); } if (opts.shaders) material["values"] = vals; ProgramInfo *prog = chooseProgram(i); TechniqueInfo techniqueInfo; bool needsNewTechnique = true; for (int j = 0; j < m_techniques.count(); ++j) { if (m_techniques[j].prog == prog) { techniqueInfo = m_techniques[j]; needsNewTechnique = opaque != techniqueInfo.opaque; } if (!needsNewTechnique) break; } if (needsNewTechnique) { QString techniqueName = newTechniqueName(); techniqueInfo = TechniqueInfo(techniqueName, opaque, prog); m_techniques.append(techniqueInfo); m_usedPrograms.insert(prog); } if (opts.shaders) { if (opts.showLog) qDebug().noquote() << "Material #" << i << "->" << techniqueInfo.name; material["technique"] = techniqueInfo.name; if (opts.genCore) { material["techniqueCore"] = techniqueInfo.coreName; material["techniqueGL2"] = techniqueInfo.gl2Name; } } if (opts.commonMat) { // The built-in shaders we output are of little use in practice. // Ideally we want Qt3D's own standard materials in order to have our // models participate in lighting for example. To achieve this, output // a KHR_materials_common block which Qt3D's loader will recognize and // prefer over the shader-based techniques. if (!prog->commonTechniqueName.isEmpty()) { QJsonObject commonMat; commonMat["technique"] = prog->commonTechniqueName; // Set the values as-is. "normalmap" is our own extension, not in the spec. // However, RGB colors have to be promoted to RGBA since the spec uses // vec4, and all types are pre-defined for common material values. promoteColorsToRGBA(&vals); commonMat["values"] = vals; if (!opaque) commonMat["transparent"] = true; QJsonObject extensions; extensions["KHR_materials_common"] = commonMat; material["extensions"] = extensions; } } materials[matInfo.name] = material; } } void GltfExporter::writeShader(const QString &src, const QString &dst, const QVector > &substTab) { for (const Shader shader : shaders) { QByteArray name = src.toUtf8(); if (!qstrcmp(shader.name, name.constData())) { QString outfn = opts.outDir + dst; QFile outf(outfn); if (outf.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_files.insert(QFileInfo(outf.fileName()).fileName()); if (opts.showLog) qDebug() << "Writing" << outfn; const auto lines = QString::fromUtf8(shader.text).split('\n'); for (QString line : lines) { for (const auto &subst : substTab) line.replace(subst.first, subst.second); line += QStringLiteral("\n"); outf.write(line.toUtf8()); } } return; } } qWarning() << "ERROR: No shader found for" << src; } void GltfExporter::exportParameter(QJsonObject &dst, const QVector ¶ms) { for (const ProgramInfo::Param ¶m : params) { QJsonObject parameter; parameter["type"] = int(param.type); if (!param.semantic.isEmpty()) parameter["semantic"] = param.semantic; if (param.name == QStringLiteral("lightIntensity")) parameter["value"] = QJsonArray() << 1 << 1 << 1; if (param.name == QStringLiteral("lightPosition")) parameter["value"] = QJsonArray() << 0 << 0 << 0 << 1; dst[param.name] = parameter; } } namespace { struct ProgramNames { QString name; QString coreName; }; } void GltfExporter::exportTechniques(QJsonObject &obj, const QString &basename) { if (!opts.shaders) return; QJsonObject shaders; QHash shaderMap; for (ProgramInfo *prog : qAsConst(m_usedPrograms)) { QString newName; if (!shaderMap.contains(prog->vertShader)) { QJsonObject vertexShader; vertexShader["type"] = 35633; if (newName.isEmpty()) newName = newShaderName(); QString key = basename + QStringLiteral("_") + newName + QStringLiteral("_v"); QString fn = QString(QStringLiteral("%1.vert")).arg(key); vertexShader["uri"] = fn; writeShader(prog->vertShader, fn, m_subst_es2); if (opts.genCore) { QJsonObject coreVertexShader; QString coreKey = QString(QStringLiteral("%1_core").arg(key)); fn = QString(QStringLiteral("%1.vert")).arg(coreKey); coreVertexShader["type"] = 35633; coreVertexShader["uri"] = fn; writeShader(prog->vertShader, fn, m_subst_core); shaders[coreKey] = coreVertexShader; shaderMap.insert(QString(prog->vertShader + QStringLiteral("_core")), coreKey); } shaders[key] = vertexShader; shaderMap.insert(prog->vertShader, key); } if (!shaderMap.contains(prog->fragShader)) { QJsonObject fragmentShader; fragmentShader["type"] = 35632; if (newName.isEmpty()) newName = newShaderName(); QString key = basename + QStringLiteral("_") + newName + QStringLiteral("_f"); QString fn = QString(QStringLiteral("%1.frag")).arg(key); fragmentShader["uri"] = fn; writeShader(prog->fragShader, fn, m_subst_es2); if (opts.genCore) { QJsonObject coreFragmentShader; QString coreKey = QString(QStringLiteral("%1_core").arg(key)); fn = QString(QStringLiteral("%1.frag")).arg(coreKey); coreFragmentShader["type"] = 35632; coreFragmentShader["uri"] = fn; writeShader(prog->fragShader, fn, m_subst_core); shaders[coreKey] = coreFragmentShader; shaderMap.insert(QString(prog->fragShader + QStringLiteral("_core")), coreKey); } shaders[key] = fragmentShader; shaderMap.insert(prog->fragShader, key); } } obj["shaders"] = shaders; QJsonObject programs; QHash programMap; for (const ProgramInfo *prog : qAsConst(m_usedPrograms)) { QJsonObject program; program["vertexShader"] = shaderMap[prog->vertShader]; program["fragmentShader"] = shaderMap[prog->fragShader]; QJsonArray attrs; for (const ProgramInfo::Param ¶m : prog->attributes) { attrs << param.nameInShader; } program["attributes"] = attrs; QString programName = newProgramName(); programMap[prog].name = programName; programs[programMap[prog].name] = program; if (opts.genCore) { program["vertexShader"] = shaderMap[QString(prog->vertShader + QLatin1String("_core"))]; program["fragmentShader"] = shaderMap[QString(prog->fragShader + QLatin1String("_core"))]; QJsonArray attrs; for (const ProgramInfo::Param ¶m : prog->attributes) { attrs << param.nameInShader; } program["attributes"] = attrs; programMap[prog].coreName = programName + QLatin1String("_core"); programs[programMap[prog].coreName] = program; } } obj["programs"] = programs; QJsonObject techniques; for (const TechniqueInfo &techniqueInfo : qAsConst(m_techniques)) { QJsonObject technique; QJsonObject parameters; const ProgramInfo *prog = techniqueInfo.prog; exportParameter(parameters, prog->attributes); exportParameter(parameters, prog->uniforms); technique["parameters"] = parameters; technique["program"] = programMap[prog].name; QJsonObject progAttrs; for (const ProgramInfo::Param ¶m : prog->attributes) { progAttrs[param.nameInShader] = param.name; } technique["attributes"] = progAttrs; QJsonObject progUniforms; for (const ProgramInfo::Param ¶m : prog->uniforms) { progUniforms[param.nameInShader] = param.name; } technique["uniforms"] = progUniforms; QJsonObject states; QJsonArray enabledStates; enabledStates << GLT_DEPTH_TEST << GLT_CULL_FACE; if (!techniqueInfo.opaque) { enabledStates << GLT_BLEND; QJsonObject funcs; // GL_ONE, GL_ONE_MINUS_SRC_ALPHA funcs["blendFuncSeparate"] = QJsonArray() << 1 << 771 << 1 << 771; states["functions"] = funcs; } states["enable"] = enabledStates; technique["states"] = states; techniques[techniqueInfo.name] = technique; if (opts.genCore) { //GL2 (same as ES2) techniques[techniqueInfo.gl2Name] = technique; //Core technique["program"] = programMap[prog].coreName; techniques[techniqueInfo.coreName] = technique; } } obj["techniques"] = techniques; } void GltfExporter::exportAnimations(QJsonObject &obj, QVector &bufList, QVector &bvList, QVector &accList) { const auto animationInfos = m_importer->animations(); if (animationInfos.empty()) { obj["animations"] = QJsonObject(); return; } QString bvName = newBufferViewName(); QByteArray extraData; int sz = 0; for (const Importer::AnimationInfo &ai : animationInfos) sz += ai.keyFrames.count() * (1 + 3 + 4 + 3) * sizeof(float); extraData.resize(sz); float *base = reinterpret_cast(extraData.data()); float *p = base; QJsonObject animations; for (const Importer::AnimationInfo &ai : animationInfos) { QJsonObject animation; animation["name"] = ai.name; animation["count"] = ai.keyFrames.count(); QJsonObject samplers; QJsonArray channels; if (ai.hasTranslation) { QJsonObject sampler; sampler["input"] = QStringLiteral("TIME"); sampler["interpolation"] = QStringLiteral("LINEAR"); sampler["output"] = QStringLiteral("translation"); samplers["sampler_translation"] = sampler; QJsonObject channel; channel["sampler"] = QStringLiteral("sampler_translation"); QJsonObject target; target["id"] = ai.targetNode; target["path"] = QStringLiteral("translation"); channel["target"] = target; channels << channel; } if (ai.hasRotation) { QJsonObject sampler; sampler["input"] = QStringLiteral("TIME"); sampler["interpolation"] = QStringLiteral("LINEAR"); sampler["output"] = QStringLiteral("rotation"); samplers["sampler_rotation"] = sampler; QJsonObject channel; channel["sampler"] = QStringLiteral("sampler_rotation"); QJsonObject target; target["id"] = ai.targetNode; target["path"] = QStringLiteral("rotation"); channel["target"] = target; channels << channel; } if (ai.hasScale) { QJsonObject sampler; sampler["input"] = QStringLiteral("TIME"); sampler["interpolation"] = QStringLiteral("LINEAR"); sampler["output"] = QStringLiteral("scale"); samplers["sampler_scale"] = sampler; QJsonObject channel; channel["sampler"] = QStringLiteral("sampler_scale"); QJsonObject target; target["id"] = ai.targetNode; target["path"] = QStringLiteral("scale"); channel["target"] = target; channels << channel; } animation["samplers"] = samplers; animation["channels"] = channels; QJsonObject parameters; // Multiple animations sharing the same data should ideally use the // same accessors. This we unfortunately cannot do due to assimp's/our // own data structures so everything will get its own accessor and data // for now. Importer::MeshInfo::Accessor acc; acc.name = newAccessorName(); acc.bufferView = bvName; acc.count = ai.keyFrames.count(); acc.componentType = GLT_FLOAT; acc.type = QStringLiteral("SCALAR"); acc.offset = uint((p - base) * sizeof(float)); for (const Importer::KeyFrame &kf : ai.keyFrames) *p++ = kf.t; parameters["TIME"] = acc.name; accList << acc; if (ai.hasTranslation) { acc.name = newAccessorName(); acc.componentType = GLT_FLOAT; acc.type = QStringLiteral("VEC3"); acc.offset = uint((p - base) * sizeof(float)); QVector lastV; for (const Importer::KeyFrame &kf : ai.keyFrames) { const QVector *v = kf.transValid ? &kf.trans : &lastV; *p++ = v->at(0); *p++ = v->at(1); *p++ = v->at(2); if (kf.transValid) lastV = *v; } parameters["translation"] = acc.name; accList << acc; } if (ai.hasRotation) { acc.name = newAccessorName(); acc.componentType = GLT_FLOAT; acc.type = QStringLiteral("VEC4"); acc.offset = uint((p - base) * sizeof(float)); QVector lastV; for (const Importer::KeyFrame &kf : ai.keyFrames) { const QVector *v = kf.rotValid ? &kf.rot : &lastV; *p++ = v->at(1); // x *p++ = v->at(2); // y *p++ = v->at(3); // z *p++ = v->at(0); // w if (kf.rotValid) lastV = *v; } parameters["rotation"] = acc.name; accList << acc; } if (ai.hasScale) { acc.name = newAccessorName(); acc.componentType = GLT_FLOAT; acc.type = QStringLiteral("VEC3"); acc.offset = uint((p - base) * sizeof(float)); QVector lastV; for (const Importer::KeyFrame &kf : ai.keyFrames) { const QVector *v = kf.scaleValid ? &kf.scale : &lastV; *p++ = v->at(0); *p++ = v->at(1); *p++ = v->at(2); if (kf.scaleValid) lastV = *v; } parameters["scale"] = acc.name; accList << acc; } animation["parameters"] = parameters; animations[newAnimationName()] = animation; } obj["animations"] = animations; // Now all the key frame data is in extraData. Append it to the first buffer // and create a single buffer view for it. if (!extraData.isEmpty()) { if (bufList.isEmpty()) { Importer::BufferInfo b; b.name = QStringLiteral("buf"); bufList << b; } Importer::BufferInfo &buf(bufList[0]); Importer::MeshInfo::BufferView bv; bv.name = bvName; bv.offset = buf.data.size(); bv.length = uint((p - base) * sizeof(float)); bv.componentType = GLT_FLOAT; bvList << bv; extraData.resize(bv.length); buf.data += extraData; if (opts.showLog) qDebug().noquote() << "Animation data in buffer uses" << extraData.size() << "bytes"; } } void GltfExporter::save(const QString &inputFilename) { if (opts.showLog) qDebug() << "Exporting"; m_files.clear(); m_techniques.clear(); m_usedPrograms.clear(); QFile f; QString basename = QFileInfo(inputFilename).baseName(); QString bufNameTempl = basename + QStringLiteral("_%1.bin"); copyExternalTextures(inputFilename); exportEmbeddedTextures(); compressTextures(); m_obj = QJsonObject(); QVector bufList = m_importer->buffers(); QVector bvList = m_importer->bufferViews(); QVector accList = m_importer->accessors(); // Animations add data to the buffer so process them first. exportAnimations(m_obj, bufList, bvList, accList); QJsonObject asset; asset["generator"] = QString(QStringLiteral("qgltf %1")).arg(QCoreApplication::applicationVersion()); asset["version"] = QStringLiteral("1.0"); asset["premultipliedAlpha"] = true; m_obj["asset"] = asset; for (int i = 0; i < bufList.count(); ++i) { QString bufName = bufNameTempl.arg(i + 1); f.setFileName(opts.outDir + bufName); if (opts.showLog) qDebug().noquote() << (opts.compress ? "Writing (compressed)" : "Writing") << (opts.outDir + bufName); if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_files.insert(QFileInfo(f.fileName()).fileName()); QByteArray data = bufList[i].data; if (opts.compress) data = qCompress(data); f.write(data); f.close(); } } QJsonObject buffers; for (int i = 0; i < bufList.count(); ++i) { QJsonObject buffer; buffer["byteLength"] = bufList[i].data.size(); buffer["type"] = QStringLiteral("arraybuffer"); buffer["uri"] = bufNameTempl.arg(i + 1); if (opts.compress) buffer["compression"] = QStringLiteral("Qt"); buffers[bufList[i].name] = buffer; } m_obj["buffers"] = buffers; QJsonObject bufferViews; for (const Importer::MeshInfo::BufferView &bv : qAsConst(bvList)) { QJsonObject bufferView; bufferView["buffer"] = bufList[bv.bufIndex].name; bufferView["byteLength"] = int(bv.length); bufferView["byteOffset"] = int(bv.offset); if (bv.target) bufferView["target"] = int(bv.target); bufferViews[bv.name] = bufferView; } m_obj["bufferViews"] = bufferViews; QJsonObject accessors; for (const Importer::MeshInfo::Accessor &acc : qAsConst(accList)) { QJsonObject accessor; accessor["bufferView"] = acc.bufferView; accessor["byteOffset"] = int(acc.offset); accessor["byteStride"] = int(acc.stride); accessor["count"] = int(acc.count); accessor["componentType"] = int(acc.componentType); accessor["type"] = acc.type; if (!acc.minVal.isEmpty() && !acc.maxVal.isEmpty()) { accessor["min"] = vec2jsvec(acc.minVal); accessor["max"] = vec2jsvec(acc.maxVal); } accessors[acc.name] = accessor; } m_obj["accessors"] = accessors; QJsonObject meshes; for (uint i = 0; i < m_importer->meshCount(); ++i) { const Importer::MeshInfo meshInfo = m_importer->meshInfo(i); QJsonObject mesh; mesh["name"] = meshInfo.originalName; QJsonArray prims; QJsonObject prim; prim["mode"] = 4; // triangles QJsonObject attrs; for (const Importer::MeshInfo::Accessor &acc : meshInfo.accessors) { if (acc.usage != QStringLiteral("INDEX")) attrs[acc.usage] = acc.name; else prim["indices"] = acc.name; } prim["attributes"] = attrs; prim["material"] = m_importer->materialInfo(meshInfo.materialIndex).name; prims.append(prim); mesh["primitives"] = prims; meshes[meshInfo.name] = mesh; } m_obj["meshes"] = meshes; QJsonObject cameras; const auto cameraInfos = m_importer->cameraInfo(); for (const Importer::CameraInfo &camInfo : cameraInfos) { QJsonObject camera; QJsonObject persp; persp["aspect_ratio"] = camInfo.aspectRatio; persp["yfov"] = camInfo.yfov; persp["znear"] = camInfo.znear; persp["zfar"] = camInfo.zfar; camera["perspective"] = persp; camera["type"] = QStringLiteral("perspective"); cameras[camInfo.name] = camera; } m_obj["cameras"] = cameras; QJsonArray sceneNodes; QJsonObject nodes; for (const Importer::Node *n : qAsConst(m_importer->rootNode()->children)) { if (nodeIsUseful(n)) sceneNodes << exportNode(n, nodes); } m_obj["nodes"] = nodes; QJsonObject scenes; QJsonObject defaultScene; defaultScene["nodes"] = sceneNodes; scenes["defaultScene"] = defaultScene; m_obj["scenes"] = scenes; m_obj["scene"] = QStringLiteral("defaultScene"); QJsonObject materials; QHash textureNameMap; exportMaterials(materials, &textureNameMap); m_obj["materials"] = materials; QJsonObject textures; QHash imageMap; // uri -> key for (QHash::const_iterator it = textureNameMap.constBegin(); it != textureNameMap.constEnd(); ++it) { QJsonObject texture; if (!imageMap.contains(it.key())) imageMap[it.key()] = newImageName(); texture["source"] = imageMap[it.key()]; texture["format"] = 0x1908; // RGBA const bool compressed = m_compressedTextures.contains(it.key()); texture["internalFormat"] = !compressed ? 0x1908 : 0x8D64; // RGBA / ETC1 texture["sampler"] = !compressed ? QStringLiteral("sampler_mip_rep") : QStringLiteral("sampler_nonmip_rep"); texture["target"] = 3553; // TEXTURE_2D texture["type"] = 5121; // UNSIGNED_BYTE textures[it.value()] = texture; } m_obj["textures"] = textures; QJsonObject images; for (QHash::const_iterator it = imageMap.constBegin(); it != imageMap.constEnd(); ++it) { QJsonObject image; image["uri"] = m_compressedTextures.contains(it.key()) ? m_compressedTextures[it.key()] : it.key(); images[it.value()] = image; } m_obj["images"] = images; QJsonObject samplers; QJsonObject sampler; sampler["magFilter"] = 9729; // LINEAR sampler["minFilter"] = 9987; // LINEAR_MIPMAP_LINEAR sampler["wrapS"] = 10497; // REPEAT sampler["wrapT"] = 10497; samplers["sampler_mip_rep"] = sampler; // Compressed textures may not support mipmapping with GLES. if (!m_compressedTextures.isEmpty()) { sampler["minFilter"] = 9729; // LINEAR samplers["sampler_nonmip_rep"] = sampler; } m_obj["samplers"] = samplers; exportTechniques(m_obj, basename); m_doc.setObject(m_obj); QString gltfName = opts.outDir + basename + QStringLiteral(".qgltf"); f.setFileName(gltfName); if (opts.showLog) qDebug().noquote() << (opts.genBin ? "Writing (binary JSON)" : "Writing") << gltfName; if (opts.genBin) { if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { m_files.insert(QFileInfo(f.fileName()).fileName()); QByteArray json = m_doc.toBinaryData(); f.write(json); f.close(); } } else { if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { m_files.insert(QFileInfo(f.fileName()).fileName()); QByteArray json = m_doc.toJson(opts.compact ? QJsonDocument::Compact : QJsonDocument::Indented); f.write(json); f.close(); } } QString qrcName = opts.outDir + basename + QStringLiteral(".qrc"); f.setFileName(qrcName); if (opts.showLog) qDebug().noquote() << "Writing" << qrcName; if (f.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { QByteArray pre = "\n"; QByteArray post = "\n"; f.write(pre); for (const QString &file : qAsConst(m_files)) { QString line = QString(QStringLiteral(" %1\n")).arg(file); f.write(line.toUtf8()); } f.write(post); f.close(); } if (opts.showLog) qDebug() << "Done\n"; } static const char *description = "qgltf uses Assimp to import a variety of 3D model formats " "and export it into fast-to-load, optimized glTF " "assets embedded into Qt resource files.\n\n" "Note: this tool should typically not be invoked directly. Instead, " "let qmake manage it based on QT3D_MODELS in the .pro file.\n\n" "For standard Qt 3D usage the recommended options are -b -S."; int main(int argc, char **argv) { QCoreApplication app(argc, argv); app.setApplicationVersion(QStringLiteral("0.2")); app.setApplicationName(QStringLiteral("Qt glTF converter")); QCommandLineParser cmdLine; cmdLine.addHelpOption(); cmdLine.addVersionOption(); cmdLine.setApplicationDescription(QString::fromUtf8(description)); QCommandLineOption outDirOpt(QStringLiteral("d"), QStringLiteral("Place all output data into "), QStringLiteral("dir")); cmdLine.addOption(outDirOpt); QCommandLineOption binOpt(QStringLiteral("b"), QStringLiteral("Store binary JSON data in the .qgltf file")); cmdLine.addOption(binOpt); QCommandLineOption compactOpt(QStringLiteral("m"), QStringLiteral("Store compact JSON in the .qgltf file")); cmdLine.addOption(compactOpt); QCommandLineOption compOpt(QStringLiteral("c"), QStringLiteral("qCompress() vertex/index data in the .bin file")); cmdLine.addOption(compOpt); QCommandLineOption tangentOpt(QStringLiteral("t"), QStringLiteral("Generate tangent vectors")); cmdLine.addOption(tangentOpt); QCommandLineOption nonInterleavedOpt(QStringLiteral("n"), QStringLiteral("Use non-interleaved buffer layout")); cmdLine.addOption(nonInterleavedOpt); QCommandLineOption scaleOpt(QStringLiteral("e"), QStringLiteral("Scale vertices by the float scale factor "), QStringLiteral("factor")); cmdLine.addOption(scaleOpt); QCommandLineOption coreOpt(QStringLiteral("g"), QStringLiteral("Generate OpenGL 3.2+ core profile shaders too")); cmdLine.addOption(coreOpt); QCommandLineOption etc1Opt(QStringLiteral("1"), QStringLiteral("Generate ETC1 compressed textures by invoking etc1tool (PNG only)")); cmdLine.addOption(etc1Opt); QCommandLineOption noCommonMatOpt(QStringLiteral("T"), QStringLiteral("Do not generate KHR_materials_common block")); cmdLine.addOption(noCommonMatOpt); QCommandLineOption noShadersOpt(QStringLiteral("S"), QStringLiteral("Do not generate shaders/programs/techniques")); cmdLine.addOption(noShadersOpt); QCommandLineOption silentOpt(QStringLiteral("s"), QStringLiteral("Silence debug output")); cmdLine.addOption(silentOpt); cmdLine.process(app); opts.outDir = cmdLine.value(outDirOpt); opts.genBin = cmdLine.isSet(binOpt); opts.compact = cmdLine.isSet(compactOpt); opts.compress = cmdLine.isSet(compOpt); opts.genTangents = cmdLine.isSet(tangentOpt); opts.interleave = !cmdLine.isSet(nonInterleavedOpt); opts.scale = 1; if (cmdLine.isSet(scaleOpt)) { bool ok = false; float v; v = cmdLine.value(scaleOpt).toFloat(&ok); if (ok) opts.scale = v; } opts.genCore = cmdLine.isSet(coreOpt); opts.texComp = cmdLine.isSet(etc1Opt) ? Options::ETC1 : Options::NoTextureCompression; opts.commonMat = !cmdLine.isSet(noCommonMatOpt); opts.shaders = !cmdLine.isSet(noShadersOpt); opts.showLog = !cmdLine.isSet(silentOpt); if (!opts.outDir.isEmpty()) { if (!opts.outDir.endsWith('/')) opts.outDir.append('/'); QDir().mkpath(opts.outDir); } const auto fileNames = cmdLine.positionalArguments(); if (fileNames.isEmpty()) cmdLine.showHelp(); AssimpImporter importer; GltfExporter exporter(&importer); for (const QString &fn : fileNames) { if (!importer.load(fn)) { qWarning() << "Failed to import" << fn; continue; } exporter.save(fn); } return 0; }