diff --git a/libraries/baking/src/FBXBaker.cpp b/libraries/baking/src/FBXBaker.cpp index a765e66bbc..0407c8508c 100644 --- a/libraries/baking/src/FBXBaker.cpp +++ b/libraries/baking/src/FBXBaker.cpp @@ -33,65 +33,8 @@ #include "FBXBaker.h" -#ifdef _WIN32 -#pragma warning( push ) -#pragma warning( disable : 4267 ) -#endif - -#include -#include - -#ifdef _WIN32 -#pragma warning( pop ) -#endif - - -FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, - const QString& bakedOutputDir, const QString& originalOutputDir) : - _fbxURL(fbxURL), - _bakedOutputDir(bakedOutputDir), - _originalOutputDir(originalOutputDir), - _textureThreadGetter(textureThreadGetter) -{ - -} - -FBXBaker::~FBXBaker() { - if (_tempDir.exists()) { - if (!_tempDir.remove(_originalFBXFilePath)) { - qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalFBXFilePath; - } - if (!_tempDir.rmdir(".")) { - qCWarning(model_baking) << "Failed to remove temporary directory:" << _tempDir; - } - } -} - -void FBXBaker::abort() { - Baker::abort(); - - // tell our underlying TextureBaker instances to abort - // the FBXBaker will wait until all are aborted before emitting its own abort signal - for (auto& textureBaker : _bakingTextures) { - textureBaker->abort(); - } -} - -void FBXBaker::bake() { - qDebug() << "FBXBaker" << _fbxURL << "bake starting"; - - auto tempDir = PathUtils::generateTemporaryDir(); - - if (tempDir.isEmpty()) { - handleError("Failed to create a temporary directory."); - return; - } - - _tempDir = tempDir; - - _originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName()); - qDebug() << "Made temporary dir " << _tempDir; - qDebug() << "Origin file path: " << _originalFBXFilePath; +void FBXBaker::bake() { + qDebug() << "FBXBaker" << _modelURL << "bake starting"; // setup the output folder for the results of this bake setupOutputFolder(); @@ -152,7 +95,7 @@ void FBXBaker::setupOutputFolder() { } // attempt to make the output folder if (!QDir().mkpath(_originalOutputDir)) { - handleError("Failed to create FBX output folder " + _bakedOutputDir); + handleError("Failed to create FBX output folder " + _originalOutputDir); return; } } @@ -160,25 +103,25 @@ void FBXBaker::setupOutputFolder() { void FBXBaker::loadSourceFBX() { // check if the FBX is local or first needs to be downloaded - if (_fbxURL.isLocalFile()) { + if (_modelURL.isLocalFile()) { // load up the local file - QFile localFBX { _fbxURL.toLocalFile() }; + QFile localFBX { _modelURL.toLocalFile() }; - qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath; + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; if (!localFBX.exists()) { //QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), ""); - handleError("Could not find " + _fbxURL.toString()); + handleError("Could not find " + _modelURL.toString()); return; } // make a copy in the output folder if (!_originalOutputDir.isEmpty()) { - qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName(); - localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName()); + qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); + localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName()); } - localFBX.copy(_originalFBXFilePath); + localFBX.copy(_originalModelFilePath); // emit our signal to start the import of the FBX source copy emit sourceCopyReadyToLoad(); @@ -193,9 +136,9 @@ void FBXBaker::loadSourceFBX() { networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - networkRequest.setUrl(_fbxURL); + networkRequest.setUrl(_modelURL); - qCDebug(model_baking) << "Downloading" << _fbxURL; + qCDebug(model_baking) << "Downloading" << _modelURL; auto networkReply = networkAccessManager.get(networkRequest); connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply); @@ -206,20 +149,20 @@ void FBXBaker::handleFBXNetworkReply() { auto requestReply = qobject_cast(sender()); if (requestReply->error() == QNetworkReply::NoError) { - qCDebug(model_baking) << "Downloaded" << _fbxURL; + qCDebug(model_baking) << "Downloaded" << _modelURL; // grab the contents of the reply and make a copy in the output folder - QFile copyOfOriginal(_originalFBXFilePath); + QFile copyOfOriginal(_originalModelFilePath); - qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName(); + qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName(); if (!copyOfOriginal.open(QIODevice::WriteOnly)) { // add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made - handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to open " + _originalFBXFilePath + ")"); + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); return; } if (copyOfOriginal.write(requestReply->readAll()) == -1) { - handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)"); + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); return; } @@ -227,98 +170,32 @@ void FBXBaker::handleFBXNetworkReply() { copyOfOriginal.close(); if (!_originalOutputDir.isEmpty()) { - copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName()); + copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); } // emit our signal to start the import of the FBX source copy emit sourceCopyReadyToLoad(); } else { // add an error to our list stating that the FBX could not be downloaded - handleError("Failed to download " + _fbxURL.toString()); + handleError("Failed to download " + _modelURL.toString()); } } void FBXBaker::importScene() { - qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists(); + qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists(); - QFile fbxFile(_originalFBXFilePath); + QFile fbxFile(_originalModelFilePath); if (!fbxFile.open(QIODevice::ReadOnly)) { - handleError("Error opening " + _originalFBXFilePath + " for reading"); + handleError("Error opening " + _originalModelFilePath + " for reading"); return; } FBXReader reader; - qCDebug(model_baking) << "Parsing" << _fbxURL; + qCDebug(model_baking) << "Parsing" << _modelURL; _rootNode = reader._rootNode = reader.parseFBX(&fbxFile); - _geometry = reader.extractFBXGeometry({}, _fbxURL.toString()); - _textureContent = reader._textureContent; -} - -QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) { - auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); - - if (texturePath.startsWith(fbxPath)) { - // texture path is a child of the FBX path, return the texture path without the fbx path - return texturePath.mid(fbxPath.length()); - } else { - // the texture path was not a child of the FBX path, return the empty string - return ""; - } -} - -QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { - // first make sure we have a unique base name for this texture - // in case another texture referenced by this model has the same base name - auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; - - QString bakedTextureFileName { textureFileInfo.completeBaseName() }; - - if (nameMatches > 0) { - // there are already nameMatches texture with this name - // append - and that number to our baked texture file name so that it is unique - bakedTextureFileName += "-" + QString::number(nameMatches); - } - - bakedTextureFileName += BAKED_TEXTURE_EXT; - - // increment the number of name matches - ++nameMatches; - - return bakedTextureFileName; -} - -QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) { - - QUrl urlToTexture; - - auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); - - if (isEmbedded) { - urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath(); - } else { - if (textureFileInfo.exists() && textureFileInfo.isFile()) { - // set the texture URL to the local texture that we have confirmed exists - urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); - } else { - // external texture that we'll need to download or find - - // this is a relative file path which will require different handling - // depending on the location of the original FBX - if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { - // the absolute path we ran into for the texture in the FBX exists on this machine - // so use that file - urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); - } else { - // we didn't find the texture on this machine at the absolute path - // so assume that it is right beside the FBX to match the behaviour of interface - urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName()); - } - } - } - - return urlToTexture; + _geometry = reader.extractFBXGeometry({}, _modelURL.toString()); + _textureContentMap = reader._textureContent; } void FBXBaker::rewriteAndBakeSceneModels() { @@ -344,176 +221,25 @@ void FBXBaker::rewriteAndBakeSceneModels() { // TODO Pull this out of _geometry instead so we don't have to reprocess it auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false); - auto& mesh = extractedMesh.mesh; - - if (mesh.wasCompressed) { - handleError("Cannot re-bake a file that contains compressed mesh"); - return; - } - - Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size()); - Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); - Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); - - int64_t numTriangles { 0 }; - for (auto& part : mesh.parts) { - if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) { - handleWarning("Found a mesh part with invalid index data, skipping"); + + // Callback to get MaterialID + GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) { + return extractedMesh.partMaterialTextures[partIndex].first; + }; + + // Compress mesh information and store in dracoMeshNode + FBXNode dracoMeshNode; + bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback); + + // if bake fails - return, if there were errors and continue, if there were warnings. + if (!success) { + if (hasErrors()) { + return; + } else if (hasWarnings()) { continue; } - numTriangles += part.quadTrianglesIndices.size() / 3; - numTriangles += part.triangleIndices.size() / 3; } - - if (numTriangles == 0) { - continue; - } - - draco::TriangleSoupMeshBuilder meshBuilder; - - meshBuilder.Start(numTriangles); - - bool hasNormals { mesh.normals.size() > 0 }; - bool hasColors { mesh.colors.size() > 0 }; - bool hasTexCoords { mesh.texCoords.size() > 0 }; - bool hasTexCoords1 { mesh.texCoords1.size() > 0 }; - bool hasPerFaceMaterials { mesh.parts.size() > 1 - || extractedMesh.partMaterialTextures[0].first != 0 }; - bool needsOriginalIndices { hasDeformers }; - - int normalsAttributeID { -1 }; - int colorsAttributeID { -1 }; - int texCoordsAttributeID { -1 }; - int texCoords1AttributeID { -1 }; - int faceMaterialAttributeID { -1 }; - int originalIndexAttributeID { -1 }; - - const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, - 3, draco::DT_FLOAT32); - if (needsOriginalIndices) { - originalIndexAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, - 1, draco::DT_INT32); - } - - if (hasNormals) { - normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, - 3, draco::DT_FLOAT32); - } - if (hasColors) { - colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, - 3, draco::DT_FLOAT32); - } - if (hasTexCoords) { - texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, - 2, draco::DT_FLOAT32); - } - if (hasTexCoords1) { - texCoords1AttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, - 2, draco::DT_FLOAT32); - } - if (hasPerFaceMaterials) { - faceMaterialAttributeID = meshBuilder.AddAttribute( - (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, - 1, draco::DT_UINT16); - } - - - auto partIndex = 0; - draco::FaceIndex face; - for (auto& part : mesh.parts) { - const auto& matTex = extractedMesh.partMaterialTextures[partIndex]; - uint16_t materialID = matTex.first; - - auto addFace = [&](QVector& indices, int index, draco::FaceIndex face) { - int32_t idx0 = indices[index]; - int32_t idx1 = indices[index + 1]; - int32_t idx2 = indices[index + 2]; - - if (hasPerFaceMaterials) { - meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); - } - - meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, - &mesh.vertices[idx0], &mesh.vertices[idx1], - &mesh.vertices[idx2]); - - if (needsOriginalIndices) { - meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, - &mesh.originalIndices[idx0], - &mesh.originalIndices[idx1], - &mesh.originalIndices[idx2]); - } - if (hasNormals) { - meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, - &mesh.normals[idx0], &mesh.normals[idx1], - &mesh.normals[idx2]); - } - if (hasColors) { - meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, - &mesh.colors[idx0], &mesh.colors[idx1], - &mesh.colors[idx2]); - } - if (hasTexCoords) { - meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, - &mesh.texCoords[idx0], &mesh.texCoords[idx1], - &mesh.texCoords[idx2]); - } - if (hasTexCoords1) { - meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, - &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], - &mesh.texCoords1[idx2]); - } - }; - - for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { - addFace(part.quadTrianglesIndices, i, face++); - } - - for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { - addFace(part.triangleIndices, i, face++); - } - - partIndex++; - } - - auto dracoMesh = meshBuilder.Finalize(); - - if (!dracoMesh) { - handleWarning("Failed to finalize the baking of a draco Geometry node"); - continue; - } - - // we need to modify unique attribute IDs for custom attributes - // so the attributes are easily retrievable on the other side - if (hasPerFaceMaterials) { - dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); - } - - if (hasTexCoords1) { - dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); - } - - if (needsOriginalIndices) { - dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); - } - - draco::Encoder encoder; - - encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); - encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); - encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); - encoder.SetSpeedOptions(0, 5); - - draco::EncoderBuffer buffer; - encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); - - FBXNode dracoMeshNode; - dracoMeshNode.name = "DracoMesh"; - auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size())); - dracoMeshNode.properties.append(value); - + objectChild.children.push_back(dracoMeshNode); static const std::vector nodeNamesToDelete { @@ -590,69 +316,25 @@ void FBXBaker::rewriteAndBakeSceneTextures() { for (FBXNode& textureChild : object->children) { if (textureChild.name == "RelativeFilename") { + QString fbxTextureFileName { textureChild.properties.at(0).toString() }; + + // grab the ID for this texture so we can figure out the + // texture type from the loaded materials + auto textureID { object->properties[0].toString() }; + auto textureType = textureTypes[textureID]; - // use QFileInfo to easily split up the existing texture filename into its components - QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() }; - QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") }; + // Compress the texture information and return the new filename to be added into the FBX scene + auto bakedTextureFile = compressTexture(fbxTextureFileName, textureType); - if (hasErrors()) { - return; - } - - if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) { - // re-baking an FBX that already references baked textures is a fail - // so we add an error and return from here - handleError("Cannot re-bake a file that references compressed textures"); - - return; - } - - if (!image::getSupportedFormats().contains(textureFileInfo.suffix())) { - // this is a texture format we don't bake, skip it - handleWarning(fbxTextureFileName + " is not a bakeable texture format"); - continue; - } - - // make sure this texture points to something and isn't one we've already re-mapped - if (!textureFileInfo.filePath().isEmpty()) { - // check if this was an embedded texture we have already have in-memory content for - auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit()); - - // figure out the URL to this texture, embedded or external - auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName, - !textureContent.isNull()); - - QString bakedTextureFileName; - if (_remappedTexturePaths.contains(urlToTexture)) { - bakedTextureFileName = _remappedTexturePaths[urlToTexture]; - } else { - // construct the new baked texture file name and file path - // ensuring that the baked texture will have a unique name - // even if there was another texture with the same name at a different path - bakedTextureFileName = createBakedTextureFileName(textureFileInfo); - _remappedTexturePaths[urlToTexture] = bakedTextureFileName; - } - - qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName - << "to" << bakedTextureFileName; - - QString bakedTextureFilePath { - _bakedOutputDir + "/" + bakedTextureFileName - }; - - // write the new filename into the FBX scene - textureChild.properties[0] = bakedTextureFileName.toLocal8Bit(); - - if (!_bakingTextures.contains(urlToTexture)) { - _outputFiles.push_back(bakedTextureFilePath); - - // grab the ID for this texture so we can figure out the - // texture type from the loaded materials - QString textureID { object->properties[0].toByteArray() }; - auto textureType = textureTypes[textureID]; - - // bake this texture asynchronously - bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent); + // If no errors or warnings have occurred during texture compression add the filename to the FBX scene + if (!bakedTextureFile.isNull()) { + textureChild.properties[0] = bakedTextureFile; + } else { + // if bake fails - return, if there were errors and continue, if there were warnings. + if (hasErrors()) { + return; + } else if (hasWarnings()) { + continue; } } } @@ -671,172 +353,26 @@ void FBXBaker::rewriteAndBakeSceneTextures() { } } -void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, - const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { - // start a bake for this texture and add it to our list to keep track of - QSharedPointer bakingTexture { - new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent), - &TextureBaker::deleteLater - }; - - // make sure we hear when the baking texture is done or aborted - connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture); - connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture); - - // keep a shared pointer to the baking texture - _bakingTextures.insert(textureURL, bakingTexture); - - // start baking the texture on one of our available worker threads - bakingTexture->moveToThread(_textureThreadGetter()); - QMetaObject::invokeMethod(bakingTexture.data(), "bake"); -} - -void FBXBaker::handleBakedTexture() { - TextureBaker* bakedTexture = qobject_cast(sender()); - - // make sure we haven't already run into errors, and that this is a valid texture - if (bakedTexture) { - if (!shouldStop()) { - if (!bakedTexture->hasErrors()) { - if (!_originalOutputDir.isEmpty()) { - // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture - - // use the path to the texture being baked to determine if this was an embedded or a linked texture - - // it is embeddded if the texure being baked was inside a folder with the name of the FBX - // since that is the fake URL we provide when baking external textures - - if (!_fbxURL.isParentOf(bakedTexture->getTextureURL())) { - // for linked textures we want to save a copy of original texture beside the original FBX - - qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); - - // check if we have a relative path to use for the texture - auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL()); - - QFile originalTextureFile { - _originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName() - }; - - if (relativeTexturePath.length() > 0) { - // make the folders needed by the relative path - } - - if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { - qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() - << "for" << _fbxURL; - } else { - handleError("Could not save original external texture " + originalTextureFile.fileName() - + " for " + _fbxURL.toString()); - return; - } - } - } - - - // now that this texture has been baked and handled, we can remove that TextureBaker from our hash - _bakingTextures.remove(bakedTexture->getTextureURL()); - - checkIfTexturesFinished(); - } else { - // there was an error baking this texture - add it to our list of errors - _errorList.append(bakedTexture->getErrors()); - - // we don't emit finished yet so that the other textures can finish baking first - _pendingErrorEmission = true; - - // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list - _bakingTextures.remove(bakedTexture->getTextureURL()); - - // abort any other ongoing texture bakes since we know we'll end up failing - for (auto& bakingTexture : _bakingTextures) { - bakingTexture->abort(); - } - - checkIfTexturesFinished(); - } - } else { - // we have errors to attend to, so we don't do extra processing for this texture - // but we do need to remove that TextureBaker from our list - // and then check if we're done with all textures - _bakingTextures.remove(bakedTexture->getTextureURL()); - - checkIfTexturesFinished(); - } - } -} - -void FBXBaker::handleAbortedTexture() { - // grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore - TextureBaker* bakedTexture = qobject_cast(sender()); - - if (bakedTexture) { - _bakingTextures.remove(bakedTexture->getTextureURL()); - } - - // since a texture we were baking aborted, our status is also aborted - _shouldAbort.store(true); - - // abort any other ongoing texture bakes since we know we'll end up failing - for (auto& bakingTexture : _bakingTextures) { - bakingTexture->abort(); - } - - checkIfTexturesFinished(); -} - void FBXBaker::exportScene() { // save the relative path to this FBX inside our passed output folder - auto fileName = _fbxURL.fileName(); + auto fileName = _modelURL.fileName(); auto baseName = fileName.left(fileName.lastIndexOf('.')); auto bakedFilename = baseName + BAKED_FBX_EXTENSION; - _bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename; + _bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename; auto fbxData = FBXWriter::encodeFBX(_rootNode); - QFile bakedFile(_bakedFBXFilePath); + QFile bakedFile(_bakedModelFilePath); if (!bakedFile.open(QIODevice::WriteOnly)) { - handleError("Error opening " + _bakedFBXFilePath + " for writing"); + handleError("Error opening " + _bakedModelFilePath + " for writing"); return; } bakedFile.write(fbxData); - _outputFiles.push_back(_bakedFBXFilePath); + _outputFiles.push_back(_bakedModelFilePath); - qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath; -} - -void FBXBaker::checkIfTexturesFinished() { - // check if we're done everything we need to do for this FBX - // and emit our finished signal if we're done - - if (_bakingTextures.isEmpty()) { - if (shouldStop()) { - // if we're checking for completion but we have errors - // that means one or more of our texture baking operations failed - - if (_pendingErrorEmission) { - setIsFinished(true); - } - - return; - } else { - qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL; - - setIsFinished(true); - } - } -} - -void FBXBaker::setWasAborted(bool wasAborted) { - if (wasAborted != _wasAborted.load()) { - Baker::setWasAborted(wasAborted); - - if (wasAborted) { - qCDebug(model_baking) << "Aborted baking" << _fbxURL; - } - } + qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath; } diff --git a/libraries/baking/src/FBXBaker.h b/libraries/baking/src/FBXBaker.h index a6034ee2b7..2888a60f73 100644 --- a/libraries/baking/src/FBXBaker.h +++ b/libraries/baking/src/FBXBaker.h @@ -19,7 +19,7 @@ #include "Baker.h" #include "TextureBaker.h" - +#include "ModelBaker.h" #include "ModelBakingLoggingCategory.h" #include @@ -30,21 +30,13 @@ static const QString BAKED_FBX_EXTENSION = ".baked.fbx"; using TextureBakerThreadGetter = std::function; -class FBXBaker : public Baker { +class FBXBaker : public ModelBaker { Q_OBJECT public: - FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter, - const QString& bakedOutputDir, const QString& originalOutputDir = ""); - ~FBXBaker() override; - - QUrl getFBXUrl() const { return _fbxURL; } - QString getBakedFBXFilePath() const { return _bakedFBXFilePath; } - - virtual void setWasAborted(bool wasAborted) override; + using ModelBaker::ModelBaker; public slots: virtual void bake() override; - virtual void abort() override; signals: void sourceCopyReadyToLoad(); @@ -52,8 +44,6 @@ signals: private slots: void bakeSourceCopy(); void handleFBXNetworkReply(); - void handleBakedTexture(); - void handleAbortedTexture(); private: void setupOutputFolder(); @@ -64,38 +54,12 @@ private: void rewriteAndBakeSceneModels(); void rewriteAndBakeSceneTextures(); void exportScene(); - void removeEmbeddedMediaFolder(); - - void checkIfTexturesFinished(); - - QString createBakedTextureFileName(const QFileInfo& textureFileInfo); - QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); - - void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir, - const QString& bakedFilename, const QByteArray& textureContent = QByteArray()); - - QUrl _fbxURL; FBXNode _rootNode; FBXGeometry* _geometry; - QHash _textureContent; - - QString _bakedFBXFilePath; - - QString _bakedOutputDir; - - // If set, the original FBX and textures will also be copied here - QString _originalOutputDir; - - QDir _tempDir; - QString _originalFBXFilePath; - - QMultiHash> _bakingTextures; QHash _textureNameMatchCount; QHash _remappedTexturePaths; - TextureBakerThreadGetter _textureThreadGetter; - bool _pendingErrorEmission { false }; }; diff --git a/libraries/baking/src/ModelBaker.cpp b/libraries/baking/src/ModelBaker.cpp new file mode 100644 index 0000000000..16a0c89c7f --- /dev/null +++ b/libraries/baking/src/ModelBaker.cpp @@ -0,0 +1,521 @@ +// +// ModelBaker.cpp +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/29/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ModelBaker.h" + +#include + +#include +#include + +#ifdef _WIN32 +#pragma warning( push ) +#pragma warning( disable : 4267 ) +#endif + +#include +#include + +#ifdef _WIN32 +#pragma warning( pop ) +#endif + +ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory) : + _modelURL(inputModelURL), + _bakedOutputDir(bakedOutputDirectory), + _originalOutputDir(originalOutputDirectory), + _textureThreadGetter(inputTextureThreadGetter) +{ + auto tempDir = PathUtils::generateTemporaryDir(); + + if (tempDir.isEmpty()) { + handleError("Failed to create a temporary directory."); + return; + } + + _modelTempDir = tempDir; + + _originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName()); + qDebug() << "Made temporary dir " << _modelTempDir; + qDebug() << "Origin file path: " << _originalModelFilePath; + +} + +ModelBaker::~ModelBaker() { + if (_modelTempDir.exists()) { + if (!_modelTempDir.remove(_originalModelFilePath)) { + qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath; + } + if (!_modelTempDir.rmdir(".")) { + qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir; + } + } +} + +void ModelBaker::abort() { + Baker::abort(); + + // tell our underlying TextureBaker instances to abort + // the ModelBaker will wait until all are aborted before emitting its own abort signal + for (auto& textureBaker : _bakingTextures) { + textureBaker->abort(); + } +} + +bool ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) { + if (mesh.wasCompressed) { + handleError("Cannot re-bake a file that contains compressed mesh"); + return false; + } + + Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size()); + Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size()); + Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size()); + + int64_t numTriangles{ 0 }; + for (auto& part : mesh.parts) { + if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) { + handleWarning("Found a mesh part with invalid index data, skipping"); + continue; + } + numTriangles += part.quadTrianglesIndices.size() / 3; + numTriangles += part.triangleIndices.size() / 3; + } + + if (numTriangles == 0) { + return false; + } + + draco::TriangleSoupMeshBuilder meshBuilder; + + meshBuilder.Start(numTriangles); + + bool hasNormals{ mesh.normals.size() > 0 }; + bool hasColors{ mesh.colors.size() > 0 }; + bool hasTexCoords{ mesh.texCoords.size() > 0 }; + bool hasTexCoords1{ mesh.texCoords1.size() > 0 }; + bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true; + bool needsOriginalIndices{ hasDeformers }; + + int normalsAttributeID { -1 }; + int colorsAttributeID { -1 }; + int texCoordsAttributeID { -1 }; + int texCoords1AttributeID { -1 }; + int faceMaterialAttributeID { -1 }; + int originalIndexAttributeID { -1 }; + + const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION, + 3, draco::DT_FLOAT32); + if (needsOriginalIndices) { + originalIndexAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX, + 1, draco::DT_INT32); + } + + if (hasNormals) { + normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL, + 3, draco::DT_FLOAT32); + } + if (hasColors) { + colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR, + 3, draco::DT_FLOAT32); + } + if (hasTexCoords) { + texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD, + 2, draco::DT_FLOAT32); + } + if (hasTexCoords1) { + texCoords1AttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1, + 2, draco::DT_FLOAT32); + } + if (hasPerFaceMaterials) { + faceMaterialAttributeID = meshBuilder.AddAttribute( + (draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID, + 1, draco::DT_UINT16); + } + + auto partIndex = 0; + draco::FaceIndex face; + uint16_t materialID; + + for (auto& part : mesh.parts) { + materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex; + + auto addFace = [&](QVector& indices, int index, draco::FaceIndex face) { + int32_t idx0 = indices[index]; + int32_t idx1 = indices[index + 1]; + int32_t idx2 = indices[index + 2]; + + if (hasPerFaceMaterials) { + meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID); + } + + meshBuilder.SetAttributeValuesForFace(positionAttributeID, face, + &mesh.vertices[idx0], &mesh.vertices[idx1], + &mesh.vertices[idx2]); + + if (needsOriginalIndices) { + meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face, + &mesh.originalIndices[idx0], + &mesh.originalIndices[idx1], + &mesh.originalIndices[idx2]); + } + if (hasNormals) { + meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face, + &mesh.normals[idx0], &mesh.normals[idx1], + &mesh.normals[idx2]); + } + if (hasColors) { + meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face, + &mesh.colors[idx0], &mesh.colors[idx1], + &mesh.colors[idx2]); + } + if (hasTexCoords) { + meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face, + &mesh.texCoords[idx0], &mesh.texCoords[idx1], + &mesh.texCoords[idx2]); + } + if (hasTexCoords1) { + meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face, + &mesh.texCoords1[idx0], &mesh.texCoords1[idx1], + &mesh.texCoords1[idx2]); + } + }; + + for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) { + addFace(part.quadTrianglesIndices, i, face++); + } + + for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) { + addFace(part.triangleIndices, i, face++); + } + + partIndex++; + } + + auto dracoMesh = meshBuilder.Finalize(); + + if (!dracoMesh) { + handleWarning("Failed to finalize the baking of a draco Geometry node"); + return false; + } + + // we need to modify unique attribute IDs for custom attributes + // so the attributes are easily retrievable on the other side + if (hasPerFaceMaterials) { + dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID); + } + + if (hasTexCoords1) { + dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1); + } + + if (needsOriginalIndices) { + dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX); + } + + draco::Encoder encoder; + + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14); + encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10); + encoder.SetSpeedOptions(0, 5); + + draco::EncoderBuffer buffer; + encoder.EncodeMeshToBuffer(*dracoMesh, &buffer); + + FBXNode dracoNode; + dracoNode.name = "DracoMesh"; + auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size())); + dracoNode.properties.append(value); + + dracoMeshNode = dracoNode; + // Mesh compression successful return true + return true; +} + +QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) { + + QFileInfo modelTextureFileInfo{ modelTextureFileName.replace("\\", "/") }; + + if (modelTextureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) { + // re-baking a model that already references baked textures + // this is an error - return from here + handleError("Cannot re-bake a file that already references compressed textures"); + return QString::null; + } + + if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) { + // this is a texture format we don't bake, skip it + handleWarning(modelTextureFileName + " is not a bakeable texture format"); + return QString::null; + } + + // make sure this texture points to something and isn't one we've already re-mapped + QString textureChild { QString::null }; + if (!modelTextureFileInfo.filePath().isEmpty()) { + // check if this was an embedded texture that we already have in-memory content for + QByteArray textureContent; + + // figure out the URL to this texture, embedded or external + if (!modelTextureFileInfo.filePath().isEmpty()) { + textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit()); + } + auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull()); + + QString bakedTextureFileName; + if (_remappedTexturePaths.contains(urlToTexture)) { + bakedTextureFileName = _remappedTexturePaths[urlToTexture]; + } else { + // construct the new baked texture file name and file path + // ensuring that the baked texture will have a unique name + // even if there was another texture with the same name at a different path + bakedTextureFileName = createBakedTextureFileName(modelTextureFileInfo); + _remappedTexturePaths[urlToTexture] = bakedTextureFileName; + } + + qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName + << "to" << bakedTextureFileName; + + QString bakedTextureFilePath{ + _bakedOutputDir + "/" + bakedTextureFileName + }; + + textureChild = bakedTextureFileName; + + if (!_bakingTextures.contains(urlToTexture)) { + _outputFiles.push_back(bakedTextureFilePath); + + // bake this texture asynchronously + bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent); + } + } + + return textureChild; +} + +void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, + const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) { + + // start a bake for this texture and add it to our list to keep track of + QSharedPointer bakingTexture{ + new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent), + &TextureBaker::deleteLater + }; + + // make sure we hear when the baking texture is done or aborted + connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture); + connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture); + + // keep a shared pointer to the baking texture + _bakingTextures.insert(textureURL, bakingTexture); + + // start baking the texture on one of our available worker threads + bakingTexture->moveToThread(_textureThreadGetter()); + QMetaObject::invokeMethod(bakingTexture.data(), "bake"); +} + +void ModelBaker::handleBakedTexture() { + TextureBaker* bakedTexture = qobject_cast(sender()); + qDebug() << "Handling baked texture" << bakedTexture->getTextureURL(); + + // make sure we haven't already run into errors, and that this is a valid texture + if (bakedTexture) { + if (!shouldStop()) { + if (!bakedTexture->hasErrors()) { + if (!_originalOutputDir.isEmpty()) { + // we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture + + // use the path to the texture being baked to determine if this was an embedded or a linked texture + + // it is embeddded if the texure being baked was inside a folder with the name of the model + // since that is the fake URL we provide when baking external textures + + if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) { + // for linked textures we want to save a copy of original texture beside the original model + + qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL(); + + // check if we have a relative path to use for the texture + auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL()); + + QFile originalTextureFile{ + _originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName() + }; + + if (relativeTexturePath.length() > 0) { + // make the folders needed by the relative path + } + + if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) { + qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName() + << "for" << _modelURL; + } else { + handleError("Could not save original external texture " + originalTextureFile.fileName() + + " for " + _modelURL.toString()); + return; + } + } + } + + + // now that this texture has been baked and handled, we can remove that TextureBaker from our hash + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } else { + // there was an error baking this texture - add it to our list of errors + _errorList.append(bakedTexture->getErrors()); + + // we don't emit finished yet so that the other textures can finish baking first + _pendingErrorEmission = true; + + // now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list + _bakingTextures.remove(bakedTexture->getTextureURL()); + + // abort any other ongoing texture bakes since we know we'll end up failing + for (auto& bakingTexture : _bakingTextures) { + bakingTexture->abort(); + } + + checkIfTexturesFinished(); + } + } else { + // we have errors to attend to, so we don't do extra processing for this texture + // but we do need to remove that TextureBaker from our list + // and then check if we're done with all textures + _bakingTextures.remove(bakedTexture->getTextureURL()); + + checkIfTexturesFinished(); + } + } +} + +void ModelBaker::handleAbortedTexture() { + // grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore + TextureBaker* bakedTexture = qobject_cast(sender()); + + qDebug() << "Texture aborted: " << bakedTexture->getTextureURL(); + + if (bakedTexture) { + _bakingTextures.remove(bakedTexture->getTextureURL()); + } + + // since a texture we were baking aborted, our status is also aborted + _shouldAbort.store(true); + + // abort any other ongoing texture bakes since we know we'll end up failing + for (auto& bakingTexture : _bakingTextures) { + bakingTexture->abort(); + } + + checkIfTexturesFinished(); +} + +QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) { + QUrl urlToTexture; + + // use QFileInfo to easily split up the existing texture filename into its components + auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/")); + + if (isEmbedded) { + urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath(); + } else { + if (textureFileInfo.exists() && textureFileInfo.isFile()) { + // set the texture URL to the local texture that we have confirmed exists + urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath()); + } else { + // external texture that we'll need to download or find + + // this is a relative file path which will require different handling + // depending on the location of the original model + if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) { + // the absolute path we ran into for the texture in the model exists on this machine + // so use that file + urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath()); + } else { + // we didn't find the texture on this machine at the absolute path + // so assume that it is right beside the model to match the behaviour of interface + urlToTexture = _modelURL.resolved(apparentRelativePath.fileName()); + } + } + } + + return urlToTexture; +} + +QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) { + auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment); + + if (texturePath.startsWith(modelPath)) { + // texture path is a child of the model path, return the texture path without the model path + return texturePath.mid(modelPath.length()); + } else { + // the texture path was not a child of the model path, return the empty string + return ""; + } +} + +void ModelBaker::checkIfTexturesFinished() { + // check if we're done everything we need to do for this model + // and emit our finished signal if we're done + + if (_bakingTextures.isEmpty()) { + if (shouldStop()) { + // if we're checking for completion but we have errors + // that means one or more of our texture baking operations failed + + if (_pendingErrorEmission) { + setIsFinished(true); + } + + return; + } else { + qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL; + + setIsFinished(true); + } + } +} + +QString ModelBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) { + // first make sure we have a unique base name for this texture + // in case another texture referenced by this model has the same base name + auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()]; + + QString bakedTextureFileName{ textureFileInfo.completeBaseName() }; + + if (nameMatches > 0) { + // there are already nameMatches texture with this name + // append - and that number to our baked texture file name so that it is unique + bakedTextureFileName += "-" + QString::number(nameMatches); + } + + bakedTextureFileName += BAKED_TEXTURE_EXT; + + // increment the number of name matches + ++nameMatches; + + return bakedTextureFileName; +} + +void ModelBaker::setWasAborted(bool wasAborted) { + if (wasAborted != _wasAborted.load()) { + Baker::setWasAborted(wasAborted); + + if (wasAborted) { + qCDebug(model_baking) << "Aborted baking" << _modelURL; + } + } +} diff --git a/libraries/baking/src/ModelBaker.h b/libraries/baking/src/ModelBaker.h new file mode 100644 index 0000000000..6fd529af92 --- /dev/null +++ b/libraries/baking/src/ModelBaker.h @@ -0,0 +1,79 @@ +// +// ModelBaker.h +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/29/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ModelBaker_h +#define hifi_ModelBaker_h + +#include +#include +#include +#include + +#include "Baker.h" +#include "TextureBaker.h" + +#include "ModelBakingLoggingCategory.h" + +#include + +#include + +using TextureBakerThreadGetter = std::function; +using GetMaterialIDCallback = std::function ; + +class ModelBaker : public Baker { + Q_OBJECT + +public: + ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter, + const QString& bakedOutputDirectory, const QString& originalOutputDirectory = ""); + virtual ~ModelBaker(); + + bool compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr); + QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE); + virtual void setWasAborted(bool wasAborted) override; + + QUrl getModelURL() const { return _modelURL; } + QString getBakedModelFilePath() const { return _bakedModelFilePath; } + +public slots: + virtual void abort() override; + +protected: + void checkIfTexturesFinished(); + + QHash _textureContentMap; + QUrl _modelURL; + QString _bakedOutputDir; + QString _originalOutputDir; + QString _bakedModelFilePath; + QDir _modelTempDir; + QString _originalModelFilePath; + +private slots: + void handleBakedTexture(); + void handleAbortedTexture(); + +private: + QString createBakedTextureFileName(const QFileInfo & textureFileInfo); + QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false); + void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir, + const QString & bakedFilename, const QByteArray & textureContent); + QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL); + + TextureBakerThreadGetter _textureThreadGetter; + QMultiHash> _bakingTextures; + QHash _textureNameMatchCount; + QHash _remappedTexturePaths; + bool _pendingErrorEmission{ false }; +}; + +#endif // hifi_ModelBaker_h diff --git a/libraries/baking/src/OBJBaker.cpp b/libraries/baking/src/OBJBaker.cpp new file mode 100644 index 0000000000..85771ff2e3 --- /dev/null +++ b/libraries/baking/src/OBJBaker.cpp @@ -0,0 +1,404 @@ +// +// OBJBaker.cpp +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/29/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include + +#include "OBJBaker.h" +#include "OBJReader.h" +#include "FBXWriter.h" + +const double UNIT_SCALE_FACTOR = 100.0; +const QByteArray PROPERTIES70_NODE_NAME = "Properties70"; +const QByteArray P_NODE_NAME = "P"; +const QByteArray C_NODE_NAME = "C"; +const QByteArray FBX_HEADER_EXTENSION = "FBXHeaderExtension"; +const QByteArray GLOBAL_SETTINGS_NODE_NAME = "GlobalSettings"; +const QByteArray OBJECTS_NODE_NAME = "Objects"; +const QByteArray GEOMETRY_NODE_NAME = "Geometry"; +const QByteArray MODEL_NODE_NAME = "Model"; +const QByteArray MATERIAL_NODE_NAME = "Material"; +const QByteArray TEXTURE_NODE_NAME = "Texture"; +const QByteArray TEXTURENAME_NODE_NAME = "TextureName"; +const QByteArray RELATIVEFILENAME_NODE_NAME = "RelativeFilename"; +const QByteArray CONNECTIONS_NODE_NAME = "Connections"; +const QByteArray CONNECTIONS_NODE_PROPERTY = "OO"; +const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP"; +const QByteArray MESH = "Mesh"; + +void OBJBaker::bake() { + qDebug() << "OBJBaker" << _modelURL << "bake starting"; + + // trigger bakeOBJ once OBJ is loaded + connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ); + + // make a local copy of the OBJ + loadOBJ(); +} + +void OBJBaker::loadOBJ() { + if (!QDir().mkpath(_bakedOutputDir)) { + handleError("Failed to create baked OBJ output folder " + _bakedOutputDir); + return; + } + + if (!QDir().mkpath(_originalOutputDir)) { + handleError("Failed to create original OBJ output folder " + _originalOutputDir); + return; + } + + // check if the OBJ is local or it needs to be downloaded + if (_modelURL.isLocalFile()) { + // loading the local OBJ + QFile localOBJ { _modelURL.toLocalFile() }; + + qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath; + + if (!localOBJ.exists()) { + handleError("Could not find " + _modelURL.toString()); + return; + } + + // make a copy in the output folder + if (!_originalOutputDir.isEmpty()) { + qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName(); + localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName()); + } + + localOBJ.copy(_originalModelFilePath); + + // local OBJ is loaded emit signal to trigger its baking + emit OBJLoaded(); + } else { + // OBJ is remote, start download + auto& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkRequest networkRequest; + + // setup the request to follow re-directs and always hit the network + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + networkRequest.setUrl(_modelURL); + + qCDebug(model_baking) << "Downloading" << _modelURL; + auto networkReply = networkAccessManager.get(networkRequest); + + connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply); + } +} + +void OBJBaker::handleOBJNetworkReply() { + auto requestReply = qobject_cast(sender()); + + if (requestReply->error() == QNetworkReply::NoError) { + qCDebug(model_baking) << "Downloaded" << _modelURL; + + // grab the contents of the reply and make a copy in the output folder + QFile copyOfOriginal(_originalModelFilePath); + + qDebug(model_baking) << "Writing copy of original obj to" << _originalModelFilePath << copyOfOriginal.fileName(); + + if (!copyOfOriginal.open(QIODevice::WriteOnly)) { + // add an error to the error list for this obj stating that a duplicate of the original obj could not be made + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")"); + return; + } + if (copyOfOriginal.write(requestReply->readAll()) == -1) { + handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)"); + return; + } + + // close that file now that we are done writing to it + copyOfOriginal.close(); + + if (!_originalOutputDir.isEmpty()) { + copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName()); + } + + // remote OBJ is loaded emit signal to trigger its baking + emit OBJLoaded(); + } else { + // add an error to our list stating that the OBJ could not be downloaded + handleError("Failed to download " + _modelURL.toString()); + } +} + +void OBJBaker::bakeOBJ() { + // Read the OBJ file + QFile objFile(_originalModelFilePath); + if (!objFile.open(QIODevice::ReadOnly)) { + handleError("Error opening " + _originalModelFilePath + " for reading"); + return; + } + + QByteArray objData = objFile.readAll(); + + bool combineParts = true; // set true so that OBJReader reads material info from material library + OBJReader reader; + auto geometry = reader.readOBJ(objData, QVariantHash(), combineParts, _modelURL); + + // Write OBJ Data as FBX tree nodes + FBXNode rootNode; + createFBXNodeTree(rootNode, *geometry); + + // Serialize the resultant FBX tree + auto encodedFBX = FBXWriter::encodeFBX(rootNode); + + // Export as baked FBX + auto fileName = _modelURL.fileName(); + auto baseName = fileName.left(fileName.lastIndexOf('.')); + auto bakedFilename = baseName + ".baked.fbx"; + + _bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename; + + QFile bakedFile; + bakedFile.setFileName(_bakedModelFilePath); + if (!bakedFile.open(QIODevice::WriteOnly)) { + handleError("Error opening " + _bakedModelFilePath + " for writing"); + return; + } + + bakedFile.write(encodedFBX); + + // Export successful + _outputFiles.push_back(_bakedModelFilePath); + qCDebug(model_baking) << "Exported" << _modelURL << "to" << _bakedModelFilePath; + + checkIfTexturesFinished(); +} + +void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) { + // Generating FBX Header Node + FBXNode headerNode; + headerNode.name = FBX_HEADER_EXTENSION; + + // Generating global settings node + // Required for Unit Scale Factor + FBXNode globalSettingsNode; + globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME; + + // Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties + FBXNode properties70Node; + properties70Node.name = PROPERTIES70_NODE_NAME; + + FBXNode pNode; + { + pNode.name = P_NODE_NAME; + pNode.properties.append({ + "UnitScaleFactor", "double", "Number", "", + UNIT_SCALE_FACTOR + }); + } + + properties70Node.children = { pNode }; + globalSettingsNode.children = { properties70Node }; + + // Generating Object node + _objectNode.name = OBJECTS_NODE_NAME; + + // Generating Object node's child - Geometry node + FBXNode geometryNode; + geometryNode.name = GEOMETRY_NODE_NAME; + { + _geometryID = nextNodeID(); + geometryNode.properties = { + _geometryID, + GEOMETRY_NODE_NAME, + MESH + }; + } + + // Compress the mesh information and store in dracoNode + bool hasDeformers = false; // No concept of deformers for an OBJ + FBXNode dracoNode; + compressMesh(geometry.meshes[0], hasDeformers, dracoNode); + geometryNode.children.append(dracoNode); + + // Generating Object node's child - Model node + FBXNode modelNode; + modelNode.name = MODEL_NODE_NAME; + { + _modelID = nextNodeID(); + modelNode.properties = { _modelID, MODEL_NODE_NAME, MESH }; + } + + _objectNode.children = { geometryNode, modelNode }; + + // Generating Objects node's child - Material node + auto& meshParts = geometry.meshes[0].parts; + for (auto& meshPart : meshParts) { + FBXNode materialNode; + materialNode.name = MATERIAL_NODE_NAME; + if (geometry.materials.size() == 1) { + // case when no material information is provided, OBJReader considers it as a single default material + for (auto& materialID : geometry.materials.keys()) { + setMaterialNodeProperties(materialNode, materialID, geometry); + } + } else { + setMaterialNodeProperties(materialNode, meshPart.materialID, geometry); + } + + _objectNode.children.append(materialNode); + } + + // Generating Texture Node + // iterate through mesh parts and process the associated textures + auto size = meshParts.size(); + for (int i = 0; i < size; i++) { + QString material = meshParts[i].materialID; + FBXMaterial currentMaterial = geometry.materials[material]; + if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) { + _textureID = nextNodeID(); + _mapTextureMaterial.emplace_back(_textureID, i); + + FBXNode textureNode; + { + textureNode.name = TEXTURE_NODE_NAME; + textureNode.properties = { _textureID }; + } + + // Texture node child - TextureName node + FBXNode textureNameNode; + { + textureNameNode.name = TEXTURENAME_NODE_NAME; + QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka"; + textureNameNode.properties = { propertyString }; + } + + // Texture node child - Relative Filename node + FBXNode relativeFilenameNode; + { + relativeFilenameNode.name = RELATIVEFILENAME_NODE_NAME; + } + + QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename; + + auto textureType = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE; + + // Compress the texture using ModelBaker::compressTexture() and store compressed file's name in the node + auto textureFile = compressTexture(textureFileName, textureType); + if (textureFile.isNull()) { + // Baking failed return + handleError("Failed to compress texture: " + textureFileName); + return; + } + relativeFilenameNode.properties = { textureFile }; + + textureNode.children = { textureNameNode, relativeFilenameNode }; + + _objectNode.children.append(textureNode); + } + } + + // Generating Connections node + FBXNode connectionsNode; + connectionsNode.name = CONNECTIONS_NODE_NAME; + + // connect Geometry to Model + FBXNode cNode; + cNode.name = C_NODE_NAME; + cNode.properties = { CONNECTIONS_NODE_PROPERTY, _geometryID, _modelID }; + connectionsNode.children = { cNode }; + + // connect all materials to model + for (auto& materialID : _materialIDs) { + FBXNode cNode; + cNode.name = C_NODE_NAME; + cNode.properties = { CONNECTIONS_NODE_PROPERTY, materialID, _modelID }; + connectionsNode.children.append(cNode); + } + + // Connect textures to materials + for (const auto& texMat : _mapTextureMaterial) { + FBXNode cAmbientNode; + cAmbientNode.name = C_NODE_NAME; + cAmbientNode.properties = { + CONNECTIONS_NODE_PROPERTY_1, + texMat.first, + _materialIDs[texMat.second], + "AmbientFactor" + }; + connectionsNode.children.append(cAmbientNode); + + FBXNode cDiffuseNode; + cDiffuseNode.name = C_NODE_NAME; + cDiffuseNode.properties = { + CONNECTIONS_NODE_PROPERTY_1, + texMat.first, + _materialIDs[texMat.second], + "DiffuseColor" + }; + connectionsNode.children.append(cDiffuseNode); + } + + // Make all generated nodes children of rootNode + rootNode.children = { globalSettingsNode, _objectNode, connectionsNode }; +} + +// Set properties for material nodes +void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry) { + auto materialID = nextNodeID(); + _materialIDs.push_back(materialID); + materialNode.properties = { materialID, material, MESH }; + + FBXMaterial currentMaterial = geometry.materials[material]; + + // Setting the hierarchy: Material -> Properties70 -> P -> Properties + FBXNode properties70Node; + properties70Node.name = PROPERTIES70_NODE_NAME; + + // Set diffuseColor + FBXNode pNodeDiffuseColor; + { + pNodeDiffuseColor.name = P_NODE_NAME; + pNodeDiffuseColor.properties.append({ + "DiffuseColor", "Color", "", "A", + currentMaterial.diffuseColor[0], currentMaterial.diffuseColor[1], currentMaterial.diffuseColor[2] + }); + } + properties70Node.children.append(pNodeDiffuseColor); + + // Set specularColor + FBXNode pNodeSpecularColor; + { + pNodeSpecularColor.name = P_NODE_NAME; + pNodeSpecularColor.properties.append({ + "SpecularColor", "Color", "", "A", + currentMaterial.specularColor[0], currentMaterial.specularColor[1], currentMaterial.specularColor[2] + }); + } + properties70Node.children.append(pNodeSpecularColor); + + // Set Shininess + FBXNode pNodeShininess; + { + pNodeShininess.name = P_NODE_NAME; + pNodeShininess.properties.append({ + "Shininess", "Number", "", "A", + currentMaterial.shininess + }); + } + properties70Node.children.append(pNodeShininess); + + // Set Opacity + FBXNode pNodeOpacity; + { + pNodeOpacity.name = P_NODE_NAME; + pNodeOpacity.properties.append({ + "Opacity", "Number", "", "A", + currentMaterial.opacity + }); + } + properties70Node.children.append(pNodeOpacity); + + materialNode.children.append(properties70Node); +} diff --git a/libraries/baking/src/OBJBaker.h b/libraries/baking/src/OBJBaker.h new file mode 100644 index 0000000000..e888c7b1d8 --- /dev/null +++ b/libraries/baking/src/OBJBaker.h @@ -0,0 +1,54 @@ +// +// OBJBaker.h +// libraries/baking/src +// +// Created by Utkarsh Gautam on 9/29/17. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_OBJBaker_h +#define hifi_OBJBaker_h + +#include "Baker.h" +#include "TextureBaker.h" +#include "ModelBaker.h" + +#include "ModelBakingLoggingCategory.h" + +using TextureBakerThreadGetter = std::function; + +using NodeID = qlonglong; + +class OBJBaker : public ModelBaker { + Q_OBJECT +public: + using ModelBaker::ModelBaker; + +public slots: + virtual void bake() override; + +signals: + void OBJLoaded(); + +private slots: + void bakeOBJ(); + void handleOBJNetworkReply(); + +private: + void loadOBJ(); + void createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry); + void setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry); + NodeID nextNodeID() { return _nodeID++; } + + NodeID _nodeID { 0 }; + NodeID _geometryID; + NodeID _modelID; + std::vector _materialIDs; + NodeID _textureID; + std::vector> _mapTextureMaterial; + FBXNode _objectNode; +}; +#endif // hifi_OBJBaker_h diff --git a/libraries/fbx/src/FBXWriter.cpp b/libraries/fbx/src/FBXWriter.cpp index 5029b489bc..511f253193 100644 --- a/libraries/fbx/src/FBXWriter.cpp +++ b/libraries/fbx/src/FBXWriter.cpp @@ -161,23 +161,19 @@ void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) { case QMetaType::QString: { auto bytes = prop.toString().toUtf8(); - out << 'S'; - out << bytes.length(); - out << bytes; + out.device()->write("S", 1); out << (int32_t)bytes.size(); out.writeRawData(bytes, bytes.size()); break; } - case QMetaType::QByteArray: - { - auto bytes = prop.toByteArray(); - out.device()->write("S", 1); - out << (int32_t)bytes.size(); - out.writeRawData(bytes, bytes.size()); - break; - } - + { + auto bytes = prop.toByteArray(); + out.device()->write("S", 1); + out << (int32_t)bytes.size(); + out.writeRawData(bytes, bytes.size()); + break; + } default: { if (prop.canConvert>()) { diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 63fb93ae46..caac08f777 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -643,13 +643,13 @@ done: } -FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url) { +FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url) { PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr); QBuffer buffer { &model }; buffer.open(QIODevice::ReadOnly); - FBXGeometry* geometryPtr = new FBXGeometry(); - FBXGeometry& geometry = *geometryPtr; + auto geometryPtr { std::make_shared() }; + FBXGeometry& geometry { *geometryPtr }; OBJTokenizer tokenizer { &buffer }; float scaleGuess = 1.0f; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index df356fada8..13ddc6e21c 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -85,7 +85,7 @@ public: QString currentMaterialName; QHash materials; - FBXGeometry* readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); + FBXGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl()); private: QUrl _url; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index d21e942581..b253ac4af3 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -190,11 +190,11 @@ void GeometryReader::run() { throw QString("empty geometry, possibly due to an unsupported FBX version"); } } else if (_url.path().toLower().endsWith(".obj")) { - fbxGeometry.reset(OBJReader().readOBJ(_data, _mapping, _combineParts, _url)); + fbxGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url); } else if (_url.path().toLower().endsWith(".obj.gz")) { QByteArray uncompressedData; - if (gunzip(_data, uncompressedData)) { - fbxGeometry.reset(OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url)); + if (gunzip(_data, uncompressedData)){ + fbxGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url); } else { throw QString("failed to decompress .obj.gz"); } diff --git a/libraries/networking/src/ResourceManager.cpp b/libraries/networking/src/ResourceManager.cpp index 317cabdc61..d06b74b724 100644 --- a/libraries/networking/src/ResourceManager.cpp +++ b/libraries/networking/src/ResourceManager.cpp @@ -26,12 +26,14 @@ #include "NetworkAccessManager.h" #include "NetworkLogging.h" -ResourceManager::ResourceManager() { +ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(atpSupportEnabled) { _thread.setObjectName("Resource Manager Thread"); - auto assetClient = DependencyManager::set(); - assetClient->moveToThread(&_thread); - QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching); + if (_atpSupportEnabled) { + auto assetClient = DependencyManager::set(); + assetClient->moveToThread(&_thread); + QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching); + } _thread.start(); } @@ -111,6 +113,10 @@ ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const Q } else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) { request = new HTTPResourceRequest(normalizedURL); } else if (scheme == URL_SCHEME_ATP) { + if (!_atpSupportEnabled) { + qCDebug(networking) << "ATP support not enabled, unable to create request for URL: " << url.url(); + return nullptr; + } request = new AssetResourceRequest(normalizedURL); } else { qCDebug(networking) << "Unknown scheme (" << scheme << ") for URL: " << url.url(); @@ -146,7 +152,7 @@ bool ResourceManager::resourceExists(const QUrl& url) { reply->deleteLater(); return reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200; - } else if (scheme == URL_SCHEME_ATP) { + } else if (scheme == URL_SCHEME_ATP && _atpSupportEnabled) { auto request = new AssetResourceRequest(url); ByteRange range; range.fromInclusive = 1; diff --git a/libraries/networking/src/ResourceManager.h b/libraries/networking/src/ResourceManager.h index 5728a7bd32..d5fa2aa0e2 100644 --- a/libraries/networking/src/ResourceManager.h +++ b/libraries/networking/src/ResourceManager.h @@ -34,7 +34,7 @@ class ResourceManager: public QObject, public Dependency { SINGLETON_DEPENDENCY public: - ResourceManager(); + ResourceManager(bool atpSupportEnabled = true); void setUrlPrefixOverride(const QString& prefix, const QString& replacement); QString normalizeURL(const QString& urlString); @@ -57,6 +57,7 @@ private: using PrefixMap = std::map; + bool _atpSupportEnabled; PrefixMap _prefixMap; QMutex _prefixMapLock; diff --git a/tools/oven/src/DomainBaker.cpp b/tools/oven/src/DomainBaker.cpp index 28ede982a0..3c6799db88 100644 --- a/tools/oven/src/DomainBaker.cpp +++ b/tools/oven/src/DomainBaker.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "DomainBaker.h" + #include #include #include @@ -17,10 +19,9 @@ #include #include "Gzip.h" - #include "Oven.h" - -#include "DomainBaker.h" +#include "FBXBaker.h" +#include "OBJBaker.h" DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName, const QString& baseOutputPath, const QUrl& destinationPath, @@ -167,15 +168,18 @@ void DomainBaker::enumerateEntities() { // check if the file pointed to by this URL is a bakeable model, by comparing extensions auto modelFileName = modelURL.fileName(); - static const QString BAKEABLE_MODEL_EXTENSION { ".fbx" }; + static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" }; + static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" }; static const QString BAKED_MODEL_EXTENSION = ".baked.fbx"; - bool isBakedFBX = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive); - bool isUnbakedFBX = modelFileName.endsWith(BAKEABLE_MODEL_EXTENSION, Qt::CaseInsensitive) && !isBakedFBX; + bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive); + bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive); + bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive); + bool isBakeable = isBakeableFBX || isBakeableOBJ; - if (isUnbakedFBX || (_shouldRebakeOriginals && isBakedFBX)) { + if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) { - if (isBakedFBX) { + if (isBakedModel) { // grab a URL to the original, that we assume is stored a directory up, in the "original" folder // with just the fbx extension qDebug() << "Re-baking original for" << modelURL; @@ -190,7 +194,7 @@ void DomainBaker::enumerateEntities() { modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment); } - // setup an FBXBaker for this URL, as long as we don't already have one + // setup a ModelBaker for this URL, as long as we don't already have one if (!_modelBakers.contains(modelURL)) { auto filename = modelURL.fileName(); auto baseName = filename.left(filename.lastIndexOf('.')); @@ -199,12 +203,23 @@ void DomainBaker::enumerateEntities() { while (QDir(_contentOutputPath + subDirName).exists()) { subDirName = "/" + baseName + "-" + QString::number(i++); } - QSharedPointer baker { - new FBXBaker(modelURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), - &FBXBaker::deleteLater - }; + + QSharedPointer baker; + if (isBakeableFBX) { + baker = { + new FBXBaker(modelURL, []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), + &FBXBaker::deleteLater + }; + } else { + baker = { + new OBJBaker(modelURL, []() -> QThread* { + return Oven::instance().getNextWorkerThread(); + }, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"), + &OBJBaker::deleteLater + }; + } // make sure our handler is called when the baker is done connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker); @@ -299,16 +314,16 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) { } void DomainBaker::handleFinishedModelBaker() { - auto baker = qobject_cast(sender()); + auto baker = qobject_cast(sender()); if (baker) { if (!baker->hasErrors()) { // this FBXBaker is done and everything went according to plan - qDebug() << "Re-writing entity references to" << baker->getFBXUrl(); + qDebug() << "Re-writing entity references to" << baker->getModelURL(); // enumerate the QJsonRef values for the URL of this FBX from our multi hash of // entity objects needing a URL re-write - for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getFBXUrl())) { + for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) { // convert the entity QJsonValueRef to a QJsonObject so we can modify its URL auto entity = entityValue.toObject(); @@ -317,7 +332,7 @@ void DomainBaker::handleFinishedModelBaker() { QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() }; // setup a new URL using the prefix we were passed - auto relativeFBXFilePath = baker->getBakedFBXFilePath().remove(_contentOutputPath); + auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath); if (relativeFBXFilePath.startsWith("/")) { relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1); } @@ -370,10 +385,10 @@ void DomainBaker::handleFinishedModelBaker() { } // remove the baked URL from the multi hash of entities needing a re-write - _entitiesNeedingRewrite.remove(baker->getFBXUrl()); + _entitiesNeedingRewrite.remove(baker->getModelURL()); // drop our shared pointer to this baker so that it gets cleaned up - _modelBakers.remove(baker->getFBXUrl()); + _modelBakers.remove(baker->getModelURL()); // emit progress to tell listeners how many models we have baked emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes); diff --git a/tools/oven/src/DomainBaker.h b/tools/oven/src/DomainBaker.h index 6426af0710..e0286a51ff 100644 --- a/tools/oven/src/DomainBaker.h +++ b/tools/oven/src/DomainBaker.h @@ -61,7 +61,7 @@ private: QJsonArray _entities; - QHash> _modelBakers; + QHash> _modelBakers; QHash> _skyboxBakers; QMultiHash _entitiesNeedingRewrite; diff --git a/tools/oven/src/Oven.cpp b/tools/oven/src/Oven.cpp index 650683e1f0..a9aa6907f1 100644 --- a/tools/oven/src/Oven.cpp +++ b/tools/oven/src/Oven.cpp @@ -9,12 +9,16 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "Oven.h" + #include #include #include -#include "Oven.h" +#include +#include +#include Oven* Oven::_staticInstance { nullptr }; @@ -29,6 +33,10 @@ Oven::Oven() { // setup our worker threads setupWorkerThreads(QThread::idealThreadCount()); + + // Initialize dependencies for OBJ Baker + DependencyManager::set(); + DependencyManager::set(false); } Oven::~Oven() { diff --git a/tools/oven/src/Oven.h b/tools/oven/src/Oven.h index effebb472e..c1acc07efb 100644 --- a/tools/oven/src/Oven.h +++ b/tools/oven/src/Oven.h @@ -14,6 +14,9 @@ #include #include +#include + +class QThread; class Oven { @@ -31,7 +34,7 @@ private: std::vector> _workerThreads; - std::atomic _nextWorkerThreadIndex; + std::atomic _nextWorkerThreadIndex; int _numWorkerThreads; static Oven* _staticInstance; diff --git a/tools/oven/src/ui/BakeWidget.cpp b/tools/oven/src/ui/BakeWidget.cpp index 22e1b1aaf9..43f4c50328 100644 --- a/tools/oven/src/ui/BakeWidget.cpp +++ b/tools/oven/src/ui/BakeWidget.cpp @@ -41,5 +41,5 @@ void BakeWidget::cancelButtonClicked() { auto stackedWidget = qobject_cast(parentWidget()); stackedWidget->removeWidget(this); - this->deleteLater(); + deleteLater(); } diff --git a/tools/oven/src/ui/ModelBakeWidget.cpp b/tools/oven/src/ui/ModelBakeWidget.cpp index 61eea55917..f80185df0f 100644 --- a/tools/oven/src/ui/ModelBakeWidget.cpp +++ b/tools/oven/src/ui/ModelBakeWidget.cpp @@ -21,18 +21,21 @@ #include #include +#include "../Oven.h" #include "../OvenGUIApplication.h" - +#include "OvenMainWindow.h" +#include "FBXBaker.h" +#include "OBJBaker.h" #include "ModelBakeWidget.h" + static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory"; static const auto MODEL_START_DIR_SETTING_KEY = "model_search_directory"; ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) : BakeWidget(parent, flags), _exportDirectory(EXPORT_DIR_SETTING_KEY), - _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) -{ + _modelStartDirectory(MODEL_START_DIR_SETTING_KEY) { setupUI(); } @@ -113,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() { startDir = QDir::homePath(); } - auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx)"); + auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)"); if (!selectedFiles.isEmpty()) { // set the contents of the model file text box to be the path to the selected file @@ -189,7 +192,7 @@ void ModelBakeWidget::bakeButtonClicked() { subFolderName = modelName + "-" + QString::number(++iteration) + "/"; } - outputDirectory.mkdir(subFolderName); + outputDirectory.mkpath(subFolderName); if (!outputDirectory.exists()) { QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory."); @@ -200,16 +203,25 @@ void ModelBakeWidget::bakeButtonClicked() { QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked"); QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original"); - + bakedOutputDirectory.mkdir("."); originalOutputDirectory.mkdir("."); - // everything seems to be in place, kick off a bake for this model now - auto baker = std::unique_ptr { - new FBXBaker(modelToBakeURL, []() -> QThread* { - return Oven::instance().getNextWorkerThread(); - }, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath()) + std::unique_ptr baker; + auto getWorkerThreadCallback = []() -> QThread* { + return Oven::instance().getNextWorkerThread(); }; + // everything seems to be in place, kick off a bake for this model now + if (modelToBakeURL.fileName().endsWith(".fbx")) { + baker.reset(new FBXBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), + originalOutputDirectory.absolutePath())); + } else if (modelToBakeURL.fileName().endsWith(".obj")) { + baker.reset(new OBJBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(), + originalOutputDirectory.absolutePath())); + } else { + qWarning() << "Unknown model type: " << modelToBakeURL.fileName(); + continue; + } // move the baker to the FBX baker thread baker->moveToThread(Oven::instance().getNextWorkerThread()); @@ -218,7 +230,7 @@ void ModelBakeWidget::bakeButtonClicked() { QMetaObject::invokeMethod(baker.get(), "bake"); // make sure we hear about the results of this baker when it is done - connect(baker.get(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker); + connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker); // add a pending row to the results window to show that this bake is in process auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); @@ -231,27 +243,31 @@ void ModelBakeWidget::bakeButtonClicked() { } void ModelBakeWidget::handleFinishedBaker() { - if (auto baker = qobject_cast(sender())) { - // add the results of this bake to the results window - auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { - return value.first.get() == baker; - }); + Baker* baker = dynamic_cast(sender()); + if (!baker) { + qWarning() << "Received signal from unexpected sender"; + return; + } - for (auto& file : baker->getOutputFiles()) { - qDebug() << "Baked file: " << file; + // add the results of this bake to the results window + auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) { + return value.first.get() == baker; + }); + + for (auto& file : baker->getOutputFiles()) { + qDebug() << "Baked file: " << file; + } + + if (it != _bakers.end()) { + auto resultRow = it->second; + auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); + + if (baker->hasErrors()) { + resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); + } else { + resultsWindow->changeStatusForRow(resultRow, "Success"); } - if (it != _bakers.end()) { - auto resultRow = it->second; - auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow(); - - if (baker->hasErrors()) { - resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n")); - } else { - resultsWindow->changeStatusForRow(resultRow, "Success"); - } - - _bakers.erase(it); - } + _bakers.erase(it); } } diff --git a/tools/oven/src/ui/ModelBakeWidget.h b/tools/oven/src/ui/ModelBakeWidget.h index b42b8725f6..fad623bf24 100644 --- a/tools/oven/src/ui/ModelBakeWidget.h +++ b/tools/oven/src/ui/ModelBakeWidget.h @@ -16,8 +16,6 @@ #include -#include - #include "BakeWidget.h" class QLineEdit; diff --git a/tools/oven/src/ui/OvenMainWindow.cpp b/tools/oven/src/ui/OvenMainWindow.cpp index dd40fb1f8f..bebc2fa7dc 100644 --- a/tools/oven/src/ui/OvenMainWindow.cpp +++ b/tools/oven/src/ui/OvenMainWindow.cpp @@ -46,7 +46,7 @@ ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) { _resultsWindow->show(); // place the results window initially below our window - _resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom()); + _resultsWindow->move(_resultsWindow->x(), frameGeometry().bottom()); } // show the results window and make sure it is in front diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 0b12cb64ed..a52e948f01 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -41,18 +41,17 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { } try { QByteArray fbxContents = fbx.readAll(); - FBXGeometry* geom; + FBXGeometry::Pointer geom; if (filename.toLower().endsWith(".obj")) { bool combineParts = false; geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts); } else if (filename.toLower().endsWith(".fbx")) { - geom = readFBX(fbxContents, QVariantHash(), filename); + geom.reset(readFBX(fbxContents, QVariantHash(), filename)); } else { qWarning() << "file has unknown extension" << filename; return false; } result = *geom; - delete geom; reSortFBXGeometryMeshes(result); } catch (const QString& error) {