mirror of
https://github.com/lubosz/overte.git
synced 2025-04-23 11:33:44 +02:00
Added code for OBJBaker and moved Texture and Mesh compression to ModelBaker superclass
This commit is contained in:
parent
23563cca47
commit
28baed18c0
9 changed files with 1452 additions and 271 deletions
|
@ -51,8 +51,7 @@ FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGet
|
|||
_fbxURL(fbxURL),
|
||||
_bakedOutputDir(bakedOutputDir),
|
||||
_originalOutputDir(originalOutputDir),
|
||||
_textureThreadGetter(textureThreadGetter)
|
||||
{
|
||||
_textureThreadGetter(textureThreadGetter) {
|
||||
|
||||
}
|
||||
|
||||
|
@ -79,7 +78,7 @@ void FBXBaker::abort() {
|
|||
|
||||
void FBXBaker::bake() {
|
||||
qDebug() << "FBXBaker" << _fbxURL << "bake starting";
|
||||
|
||||
|
||||
auto tempDir = PathUtils::generateTemporaryDir();
|
||||
|
||||
if (tempDir.isEmpty()) {
|
||||
|
@ -162,7 +161,7 @@ void FBXBaker::loadSourceFBX() {
|
|||
// check if the FBX is local or first needs to be downloaded
|
||||
if (_fbxURL.isLocalFile()) {
|
||||
// load up the local file
|
||||
QFile localFBX { _fbxURL.toLocalFile() };
|
||||
QFile localFBX{ _fbxURL.toLocalFile() };
|
||||
|
||||
qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath;
|
||||
|
||||
|
@ -273,7 +272,7 @@ QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
|
|||
// in case another texture referenced by this model has the same base name
|
||||
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
|
||||
|
||||
QString bakedTextureFileName { textureFileInfo.completeBaseName() };
|
||||
QString bakedTextureFileName{ textureFileInfo.completeBaseName() };
|
||||
|
||||
if (nameMatches > 0) {
|
||||
// there are already nameMatches texture with this name
|
||||
|
@ -297,7 +296,7 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeF
|
|||
|
||||
if (isEmbedded) {
|
||||
urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath();
|
||||
} else {
|
||||
} else {
|
||||
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
|
||||
// set the texture URL to the local texture that we have confirmed exists
|
||||
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
|
||||
|
@ -323,7 +322,7 @@ QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeF
|
|||
|
||||
void FBXBaker::rewriteAndBakeSceneModels() {
|
||||
unsigned int meshIndex = 0;
|
||||
bool hasDeformers { false };
|
||||
bool hasDeformers{ false };
|
||||
for (FBXNode& rootChild : _rootNode.children) {
|
||||
if (rootChild.name == "Objects") {
|
||||
for (FBXNode& objectChild : rootChild.children) {
|
||||
|
@ -344,178 +343,19 @@ 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");
|
||||
continue;
|
||||
}
|
||||
numTriangles += part.quadTrianglesIndices.size() / 3;
|
||||
numTriangles += part.triangleIndices.size() / 3;
|
||||
}
|
||||
|
||||
if (numTriangles == 0) {
|
||||
// Callback to get MaterialID from FBXBaker in ModelBaker
|
||||
getMaterialIDCallback materialIDcallback = [extractedMesh](int partIndex) {return extractedMesh.partMaterialTextures[partIndex].first;};
|
||||
// Compress mesh information and store in dracoMeshNode
|
||||
FBXNode* dracoMeshNode = this->compressMesh(extractedMesh.mesh, hasDeformers, materialIDcallback);
|
||||
// if dracoMeshNode is null (i.e error occurred while baking), continue iterating through Object node children
|
||||
if (!dracoMeshNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
draco::TriangleSoupMeshBuilder meshBuilder;
|
||||
objectChild.children.push_back(*dracoMeshNode);
|
||||
|
||||
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 };
|
||||
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<int>& 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<QString> nodeNamesToDelete {
|
||||
static const std::vector<QString> nodeNamesToDelete{
|
||||
// Node data that is packed into the draco mesh
|
||||
"Vertices",
|
||||
"PolygonVertexIndex",
|
||||
|
@ -548,6 +388,7 @@ void FBXBaker::rewriteAndBakeSceneModels() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
void FBXBaker::rewriteAndBakeSceneTextures() {
|
||||
using namespace image::TextureUsage;
|
||||
QHash<QString, image::TextureUsage::Type> textureTypes;
|
||||
|
@ -591,69 +432,97 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
|
|||
if (textureChild.name == "RelativeFilename") {
|
||||
|
||||
// use QFileInfo to easily split up the existing texture filename into its components
|
||||
QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() };
|
||||
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
|
||||
QString fbxTextureFileName{ textureChild.properties.at(0).toByteArray() };
|
||||
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
}
|
||||
// Callback to get texture content and type from FBXBaker in ModelBaker
|
||||
//auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
|
||||
//qCDebug(model_baking) << "TextureContent" << textureContent.size();
|
||||
|
||||
getTextureContentTypeCallback textureContentTypeCallback = [=]() {
|
||||
QPair<QByteArray, image::TextureUsage::Type> result;
|
||||
result.first = _textureContent.value(fbxTextureFileName.toLocal8Bit());;
|
||||
result.second = textureTypes[object->properties[0].toByteArray()];
|
||||
return result;
|
||||
};
|
||||
|
||||
// Compress the texture information and return the new filename to be added into the FBX scene
|
||||
QByteArray* bakedTextureFile = this->compressTexture(fbxTextureFileName, _fbxURL, _bakedOutputDir, _textureThreadGetter, textureContentTypeCallback, _originalOutputDir);
|
||||
|
||||
// If no errors or warnings have occurred during texture compression add the filename to the FBX scene
|
||||
if (bakedTextureFile) {
|
||||
textureChild.properties[0] = *bakedTextureFile;
|
||||
} else {
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
} else if (hasWarnings()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
//QFileInfo textureFileInfo{ fbxTextureFileName.replace("\\", "/") };
|
||||
|
||||
if (!TextureBaker::getSupportedFormats().contains(textureFileInfo.suffix())) {
|
||||
// this is a texture format we don't bake, skip it
|
||||
handleWarning(fbxTextureFileName + " is not a bakeable texture format");
|
||||
continue;
|
||||
}
|
||||
//if (hasErrors()) {
|
||||
// return;
|
||||
//}
|
||||
|
||||
// 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());
|
||||
//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");
|
||||
|
||||
// figure out the URL to this texture, embedded or external
|
||||
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
|
||||
!textureContent.isNull());
|
||||
// return;
|
||||
//}
|
||||
|
||||
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;
|
||||
}
|
||||
//if (!TextureBaker::getSupportedFormats().contains(textureFileInfo.suffix())) {
|
||||
// // this is a texture format we don't bake, skip it
|
||||
// handleWarning(fbxTextureFileName + " is not a bakeable texture format");
|
||||
// continue;
|
||||
//}
|
||||
|
||||
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
|
||||
<< "to" << bakedTextureFileName;
|
||||
//// 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());
|
||||
|
||||
QString bakedTextureFilePath {
|
||||
_bakedOutputDir + "/" + bakedTextureFileName
|
||||
};
|
||||
// // figure out the URL to this texture, embedded or external
|
||||
// qCDebug(model_baking) << "TextureContent" << textureContent.size();
|
||||
// auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
|
||||
// !textureContent.isNull());
|
||||
|
||||
// write the new filename into the FBX scene
|
||||
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
|
||||
// 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;
|
||||
// }
|
||||
|
||||
if (!_bakingTextures.contains(urlToTexture)) {
|
||||
_outputFiles.push_back(bakedTextureFilePath);
|
||||
// qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
|
||||
// << "to" << bakedTextureFileName;
|
||||
|
||||
// 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];
|
||||
// QString bakedTextureFilePath{
|
||||
// _bakedOutputDir + "/" + bakedTextureFileName
|
||||
// };
|
||||
|
||||
// bake this texture asynchronously
|
||||
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
|
||||
}
|
||||
}
|
||||
// // 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);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -670,10 +539,93 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
|
|||
}
|
||||
}
|
||||
|
||||
//void FBXBaker::rewriteAndBakeSceneTextures() {
|
||||
// using namespace image::TextureUsage;
|
||||
// QHash<QString, image::TextureUsage::Type> textureTypes;
|
||||
//
|
||||
// // enumerate the materials in the extracted geometry so we can determine the texture type for each texture ID
|
||||
// for (const auto& material : _geometry->materials) {
|
||||
// if (material.normalTexture.isBumpmap) {
|
||||
// textureTypes[material.normalTexture.id] = BUMP_TEXTURE;
|
||||
// } else {
|
||||
// textureTypes[material.normalTexture.id] = NORMAL_TEXTURE;
|
||||
// }
|
||||
//
|
||||
// textureTypes[material.albedoTexture.id] = ALBEDO_TEXTURE;
|
||||
// textureTypes[material.glossTexture.id] = GLOSS_TEXTURE;
|
||||
// textureTypes[material.roughnessTexture.id] = ROUGHNESS_TEXTURE;
|
||||
// textureTypes[material.specularTexture.id] = SPECULAR_TEXTURE;
|
||||
// textureTypes[material.metallicTexture.id] = METALLIC_TEXTURE;
|
||||
// textureTypes[material.emissiveTexture.id] = EMISSIVE_TEXTURE;
|
||||
// textureTypes[material.occlusionTexture.id] = OCCLUSION_TEXTURE;
|
||||
// textureTypes[material.lightmapTexture.id] = LIGHTMAP_TEXTURE;
|
||||
// }
|
||||
//
|
||||
// // enumerate the children of the root node
|
||||
// for (FBXNode& rootChild : _rootNode.children) {
|
||||
//
|
||||
// if (rootChild.name == "Objects") {
|
||||
//
|
||||
// // enumerate the objects
|
||||
// auto object = rootChild.children.begin();
|
||||
// while (object != rootChild.children.end()) {
|
||||
// if (object->name == "Texture") {
|
||||
//
|
||||
// // double check that we didn't get an abort while baking the last texture
|
||||
// if (shouldStop()) {
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // enumerate the texture children
|
||||
// for (FBXNode& textureChild : object->children) {
|
||||
//
|
||||
// if (textureChild.name == "RelativeFilename") {
|
||||
// QString fbxTextureFileName{ textureChild.properties.at(0).toByteArray() };
|
||||
//
|
||||
// // Callback to get texture content and type from FBXBaker in ModelBaker
|
||||
// auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
|
||||
// qCDebug(model_baking) << "TextureContent" << textureContent;
|
||||
//
|
||||
// getTextureContentTypeCallback textureContentTypeCallback = [fbxTextureFileName, textureTypes, object, textureContent]() {
|
||||
// QPair<QByteArray, image::TextureUsage::Type> result;
|
||||
// result.first = textureContent;
|
||||
// result.second = textureTypes[object->properties[0].toByteArray()];
|
||||
// return result;
|
||||
// };
|
||||
//
|
||||
// // Compress the texture information and return the new filename to be added into the FBX scene
|
||||
// QByteArray* bakedTextureFile = this->compressTexture(fbxTextureFileName, _fbxURL, _bakedOutputDir, _textureThreadGetter, textureContentTypeCallback, _originalOutputDir);
|
||||
//
|
||||
// // If no errors or warnings have occurred during texture compression add the filename to the FBX scene
|
||||
// if (bakedTextureFile) {
|
||||
// textureChild.properties[0] = *bakedTextureFile;
|
||||
// } else {
|
||||
// if (hasErrors()) {
|
||||
// return;
|
||||
// } else if (hasWarnings()) {
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// ++object;
|
||||
//
|
||||
// } else if (object->name == "Video") {
|
||||
// // this is an embedded texture, we need to remove it from the FBX
|
||||
// object = rootChild.children.erase(object);
|
||||
// } else {
|
||||
// ++object;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
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<TextureBaker> bakingTexture {
|
||||
QSharedPointer<TextureBaker> bakingTexture{
|
||||
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
|
||||
&TextureBaker::deleteLater
|
||||
};
|
||||
|
@ -713,7 +665,7 @@ void FBXBaker::handleBakedTexture() {
|
|||
// check if we have a relative path to use for the texture
|
||||
auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL());
|
||||
|
||||
QFile originalTextureFile {
|
||||
QFile originalTextureFile{
|
||||
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
#include "Baker.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "ModelBaker.h"
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
|
||||
#include <gpu/Texture.h>
|
||||
|
@ -30,7 +30,7 @@ static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
|
|||
|
||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
||||
|
||||
class FBXBaker : public Baker {
|
||||
class FBXBaker : public ModelBaker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
||||
|
@ -42,14 +42,14 @@ public:
|
|||
|
||||
virtual void setWasAborted(bool wasAborted) override;
|
||||
|
||||
public slots:
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
virtual void abort() override;
|
||||
|
||||
signals:
|
||||
void sourceCopyReadyToLoad();
|
||||
|
||||
private slots:
|
||||
private slots:
|
||||
void bakeSourceCopy();
|
||||
void handleFBXNetworkReply();
|
||||
void handleBakedTexture();
|
||||
|
@ -79,7 +79,7 @@ private:
|
|||
FBXNode _rootNode;
|
||||
FBXGeometry* _geometry;
|
||||
QHash<QByteArray, QByteArray> _textureContent;
|
||||
|
||||
|
||||
QString _bakedFBXFilePath;
|
||||
|
||||
QString _bakedOutputDir;
|
||||
|
@ -96,7 +96,7 @@ private:
|
|||
|
||||
TextureBakerThreadGetter _textureThreadGetter;
|
||||
|
||||
bool _pendingErrorEmission { false };
|
||||
bool _pendingErrorEmission{ false };
|
||||
};
|
||||
|
||||
#endif // hifi_FBXBaker_h
|
||||
|
|
532
libraries/baking/src/ModelBaker.cpp
Normal file
532
libraries/baking/src/ModelBaker.cpp
Normal file
|
@ -0,0 +1,532 @@
|
|||
//
|
||||
// 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 <image\Image.h>
|
||||
|
||||
#include <QtConcurrent>
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QEventLoop>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <NetworkAccessManager.h>
|
||||
#include <SharedUtil.h>
|
||||
|
||||
#include <PathUtils.h>
|
||||
|
||||
#include <FBXReader.h>
|
||||
#include <FBXWriter.h>
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "FBXBaker.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 4267 )
|
||||
#endif
|
||||
|
||||
#include <draco/mesh/triangle_soup_mesh_builder.h>
|
||||
#include <draco/compression/encode.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( pop )
|
||||
#endif
|
||||
|
||||
ModelBaker::ModelBaker() {}
|
||||
|
||||
void ModelBaker::bake() {}
|
||||
|
||||
//void ModelBaker::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();
|
||||
// }
|
||||
//}
|
||||
|
||||
FBXNode* ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, getMaterialIDCallback materialIDCallback) {
|
||||
if (mesh.wasCompressed) {
|
||||
handleError("Cannot re-bake a file that contains compressed mesh");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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 nullptr;
|
||||
}
|
||||
|
||||
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;
|
||||
if (materialIDCallback) {
|
||||
if (mesh.parts.size() > 1 || materialIDCallback(0) != 0) {
|
||||
hasPerFaceMaterials = true;
|
||||
}
|
||||
} else {
|
||||
hasPerFaceMaterials = 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) {
|
||||
if (materialIDCallback) {
|
||||
materialID = materialIDCallback(partIndex);
|
||||
} else {
|
||||
materialID = partIndex;
|
||||
}
|
||||
|
||||
auto addFace = [&](QVector<int>& 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 nullptr;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
static FBXNode dracoMeshNode;
|
||||
dracoMeshNode.name = "DracoMesh";
|
||||
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size()));
|
||||
dracoMeshNode.properties.append(value);
|
||||
|
||||
return &dracoMeshNode;
|
||||
}
|
||||
|
||||
QByteArray* ModelBaker::compressTexture(QString modelTextureFileName, QUrl modelURL, QString bakedOutputDir, TextureBakerThreadGetter textureThreadGetter,
|
||||
getTextureContentTypeCallback textureContentTypeCallback, const QString& originalOutputDir) {
|
||||
_modelURL = modelURL;
|
||||
_textureThreadGetter = textureThreadGetter;
|
||||
_originalOutputDir = originalOutputDir;
|
||||
|
||||
static QByteArray textureChild;
|
||||
|
||||
QPair<QByteArray, image::TextureUsage::Type> textureContentType;
|
||||
// grab the ID for this texture so we can figure out the
|
||||
// texture type from the loaded materials
|
||||
textureContentType = textureContentTypeCallback();
|
||||
|
||||
QByteArray textureContent = textureContentType.first;
|
||||
image::TextureUsage::Type textureType = textureContentType.second;
|
||||
|
||||
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 nullptr;
|
||||
}
|
||||
|
||||
if (!TextureBaker::getSupportedFormats().contains(modelTextureFileInfo.suffix())) {
|
||||
// this is a texture format we don't bake, skip it
|
||||
handleWarning(modelTextureFileName + " is not a bakeable texture format");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// make sure this texture points to something and isn't one we've already re-mapped
|
||||
if (!modelTextureFileInfo.filePath().isEmpty()) {
|
||||
// check if this was an embedded texture that we already have in-memory content for
|
||||
|
||||
// figure out the URL to this texture, embedded or external
|
||||
//qCDebug(model_baking) << "TextureContent" << !textureContent.isNull();
|
||||
auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName,
|
||||
true);
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
// write the new filename into the FBX scene
|
||||
textureChild = bakedTextureFileName.toLocal8Bit();
|
||||
|
||||
if (!_bakingTextures.contains(urlToTexture)) {
|
||||
_outputFiles.push_back(bakedTextureFilePath);
|
||||
|
||||
// bake this texture asynchronously
|
||||
qCDebug(model_baking) << "URLHere" << urlToTexture;
|
||||
bakeTexture(urlToTexture, textureType, bakedOutputDir, bakedTextureFileName, textureContent);
|
||||
}
|
||||
}
|
||||
|
||||
return &textureChild;
|
||||
}
|
||||
|
||||
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 FBX
|
||||
if (_modelURL.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 = _modelURL.resolved(apparentRelativePath.fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlToTexture;
|
||||
}
|
||||
|
||||
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<TextureBaker> 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<TextureBaker*>(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 (!_modelURL.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 = 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ModelBaker::texturePathRelativeToModel(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 "";
|
||||
}
|
||||
}
|
||||
|
||||
void ModelBaker::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 finished" << _modelURL;
|
||||
|
||||
setIsFinished(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<TextureBaker*>(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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
68
libraries/baking/src/ModelBaker.h
Normal file
68
libraries/baking/src/ModelBaker.h
Normal file
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// 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 <QtCore/QFutureSynchronizer>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
#include "Baker.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
|
||||
#include <gpu/Texture.h>
|
||||
|
||||
#include <FBX.h>
|
||||
|
||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
||||
using getMaterialIDCallback = std::function <int(int)>;
|
||||
using getTextureContentTypeCallback = std::function<QPair<QByteArray, image::TextureUsage::Type>()>;
|
||||
|
||||
class ModelBaker : public Baker{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ModelBaker();
|
||||
FBXNode* compressMesh(FBXMesh& mesh, bool hasDeformers, getMaterialIDCallback materialIDCallback = NULL);
|
||||
QByteArray* compressTexture(QString textureFileName, QUrl modelUrl, QString bakedOutputDir, TextureBakerThreadGetter textureThreadGetter,
|
||||
getTextureContentTypeCallback textureContentTypeCallback = NULL, const QString& originalOutputDir = "");
|
||||
virtual void setWasAborted(bool wasAborted) override;
|
||||
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
//virtual void abort() override;
|
||||
|
||||
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 fbxURL, QUrl textureURL);
|
||||
void checkIfTexturesFinished();
|
||||
|
||||
QHash<QString, int> _textureNameMatchCount;
|
||||
QHash<QUrl, QString> _remappedTexturePaths;
|
||||
QUrl _modelURL;
|
||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||
TextureBakerThreadGetter _textureThreadGetter;
|
||||
QString _originalOutputDir;
|
||||
bool _pendingErrorEmission{ false };
|
||||
};
|
||||
|
||||
#endif // hifi_ModelBaker_h
|
533
libraries/baking/src/OBJBaker.cpp
Normal file
533
libraries/baking/src/OBJBaker.cpp
Normal file
|
@ -0,0 +1,533 @@
|
|||
//
|
||||
// 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 <PathUtils.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
|
||||
#include "OBJBaker.h"
|
||||
#include "OBJReader.h"
|
||||
#include "FBXWriter.h"
|
||||
|
||||
OBJBaker::OBJBaker(const QUrl& objURL, TextureBakerThreadGetter textureThreadGetter,
|
||||
const QString& bakedOutputDir, const QString& originalOutputDir) :
|
||||
_objURL(objURL),
|
||||
_bakedOutputDir(bakedOutputDir),
|
||||
_originalOutputDir(originalOutputDir),
|
||||
_textureThreadGetter(textureThreadGetter)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
OBJBaker::~OBJBaker() {
|
||||
if (_tempDir.exists()) {
|
||||
if (!_tempDir.remove(_originalOBJFilePath)) {
|
||||
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalOBJFilePath;
|
||||
}
|
||||
if (!_tempDir.rmdir(".")) {
|
||||
qCWarning(model_baking) << "Failed to remove temporary directory:" << _tempDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OBJBaker::bake() {
|
||||
qDebug() << "OBJBaker" << _objURL << "bake starting";
|
||||
|
||||
auto tempDir = PathUtils::generateTemporaryDir();
|
||||
|
||||
if (tempDir.isEmpty()) {
|
||||
handleError("Failed to create a temporary directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
_tempDir = tempDir;
|
||||
|
||||
_originalOBJFilePath = _tempDir.filePath(_objURL.fileName());
|
||||
qDebug() << "Made temporary dir " << _tempDir;
|
||||
qDebug() << "Origin file path: " << _originalOBJFilePath;
|
||||
|
||||
// trigger startBake once OBJ is loaded
|
||||
connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::startBake);
|
||||
|
||||
// make a local copy of the OBJ
|
||||
loadOBJ();
|
||||
}
|
||||
|
||||
void OBJBaker::loadOBJ() {
|
||||
// check if the OBJ is local or it needs to be downloaded
|
||||
if (_objURL.isLocalFile()) {
|
||||
// loading the local OBJ
|
||||
QFile localOBJ{ _objURL.toLocalFile() };
|
||||
|
||||
qDebug() << "Local file url: " << _objURL << _objURL.toString() << _objURL.toLocalFile() << ", copying to: " << _originalOBJFilePath;
|
||||
|
||||
if (!localOBJ.exists()) {
|
||||
handleError("Could not find " + _objURL.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// make a copy in the output folder
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
qDebug() << "Copying to: " << _originalOutputDir << "/" << _objURL.fileName();
|
||||
localOBJ.copy(_originalOutputDir + "/" + _objURL.fileName());
|
||||
}
|
||||
|
||||
localOBJ.copy(_originalOBJFilePath);
|
||||
|
||||
// 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(_objURL);
|
||||
|
||||
qCDebug(model_baking) << "Downloading" << _objURL;
|
||||
auto networkReply = networkAccessManager.get(networkRequest);
|
||||
|
||||
connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply);
|
||||
}
|
||||
}
|
||||
|
||||
void OBJBaker::handleOBJNetworkReply() {
|
||||
auto requestReply = qobject_cast<QNetworkReply*>(sender());
|
||||
|
||||
if (requestReply->error() == QNetworkReply::NoError) {
|
||||
qCDebug(model_baking) << "Downloaded" << _objURL;
|
||||
|
||||
// grab the contents of the reply and make a copy in the output folder
|
||||
QFile copyOfOriginal(_originalOBJFilePath);
|
||||
|
||||
qDebug(model_baking) << "Writing copy of original obj to" << _originalOBJFilePath << 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 " + _objURL.toString() + " (Failed to open " + _originalOBJFilePath + ")");
|
||||
return;
|
||||
}
|
||||
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
|
||||
handleError("Could not create copy of " + _objURL.toString() + " (Failed to write)");
|
||||
return;
|
||||
}
|
||||
|
||||
// close that file now that we are done writing to it
|
||||
copyOfOriginal.close();
|
||||
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
copyOfOriginal.copy(_originalOutputDir + "/" + _objURL.fileName());
|
||||
}
|
||||
|
||||
// emit our signal to start the import of the obj source copy
|
||||
emit OBJLoaded();
|
||||
} else {
|
||||
// add an error to our list stating that the obj could not be downloaded
|
||||
handleError("Failed to download " + _objURL.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void OBJBaker::startBake() {
|
||||
// Read the OBJ
|
||||
QFile objFile(_originalOBJFilePath);
|
||||
if (!objFile.open(QIODevice::ReadOnly)) {
|
||||
handleError("Error opening " + _originalOBJFilePath + " for reading");
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray objData = objFile.readAll();
|
||||
|
||||
bool combineParts = true;
|
||||
OBJReader reader;
|
||||
FBXGeometry* geometry = reader.readOBJ(objData, QVariantHash(), combineParts, _objURL);
|
||||
|
||||
// Write OBJ Data in FBX tree nodes
|
||||
FBXNode objRoot;
|
||||
createFBXNodeTree(&objRoot, geometry);
|
||||
|
||||
// Serialize the resultant FBX tree
|
||||
auto encodedFBX = FBXWriter::encodeFBX(objRoot);
|
||||
|
||||
// Export as baked FBX
|
||||
auto fileName = _objURL.fileName();
|
||||
auto baseName = fileName.left(fileName.lastIndexOf('.'));
|
||||
auto bakedFilename = baseName + ".baked.fbx";
|
||||
|
||||
_bakedOBJFilePath = _bakedOutputDir + "/" + bakedFilename;
|
||||
|
||||
QFile bakedFile;
|
||||
bakedFile.setFileName(_bakedOBJFilePath);
|
||||
if (!bakedFile.open(QIODevice::WriteOnly)) {
|
||||
handleError("Error opening " + _bakedOBJFilePath + " for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
bakedFile.write(encodedFBX);
|
||||
|
||||
// Export successful
|
||||
_outputFiles.push_back(_bakedOBJFilePath);
|
||||
qCDebug(model_baking) << "Exported" << _objURL << "to" << _bakedOBJFilePath;
|
||||
// Export done emit finished
|
||||
emit finished();
|
||||
}
|
||||
|
||||
void OBJBaker::createFBXNodeTree(FBXNode* objRoot, FBXGeometry* geometry) {
|
||||
// Generating FBX Header Node
|
||||
FBXNode headerNode;
|
||||
headerNode.name = "FBXHeaderExtension";
|
||||
|
||||
// Generating global settings node
|
||||
// Required for Unit Scale Factor
|
||||
FBXNode globalSettingsNode;
|
||||
globalSettingsNode.name = "GlobalSettings";
|
||||
FBXNode properties70Node;
|
||||
properties70Node.name = "Properties70";
|
||||
FBXNode pNode;
|
||||
pNode.name = "P";
|
||||
setProperties(&pNode);
|
||||
properties70Node.children = { pNode };
|
||||
globalSettingsNode.children = { properties70Node };
|
||||
|
||||
// Generating Object node
|
||||
_objectNode.name = "Objects";
|
||||
|
||||
// Generating Object node's child - Geometry node
|
||||
FBXNode geometryNode;
|
||||
geometryNode.name = "Geometry";
|
||||
setProperties(&geometryNode);
|
||||
|
||||
// Compress the mesh information and store in dracoNode
|
||||
bool hasDeformers = false;
|
||||
ModelBaker modelBaker;
|
||||
FBXNode* dracoNode = this->compressMesh(geometry->meshes[0], hasDeformers);
|
||||
geometryNode.children.append(*dracoNode);
|
||||
|
||||
// Generating Object node's child - Model node
|
||||
FBXNode modelNode;
|
||||
modelNode.name = "Model";
|
||||
setProperties(&modelNode);
|
||||
_objectNode.children = { geometryNode, modelNode };
|
||||
|
||||
// Generating Objects node's child - Material node
|
||||
QVector<FBXMeshPart> meshParts = geometry->meshes[0].parts;
|
||||
for (auto p : meshParts) {
|
||||
FBXNode materialNode;
|
||||
materialNode.name = "Material";
|
||||
if (geometry->materials.size() == 1) {
|
||||
foreach(QString materialID, geometry->materials.keys()) {
|
||||
setMaterialNodeProperties(&materialNode, materialID, geometry);
|
||||
}
|
||||
} else {
|
||||
setMaterialNodeProperties(&materialNode, p.materialID, geometry);
|
||||
}
|
||||
_objectNode.children.append(materialNode);
|
||||
}
|
||||
|
||||
// Texture Node
|
||||
int count = 0;
|
||||
for (int i = 0;i < meshParts.size();i++) {
|
||||
QString material = meshParts[i].materialID;
|
||||
FBXMaterial currentMaterial = geometry->materials[material];
|
||||
if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) {
|
||||
_textureID = _nodeID;
|
||||
mapTextureMaterial.push_back(QPair<qlonglong, int>(_textureID, i));
|
||||
QVariant property0(_nodeID++);
|
||||
FBXNode textureNode;
|
||||
textureNode.name = "Texture";
|
||||
textureNode.properties = { property0 };
|
||||
|
||||
FBXNode textureNameNode;
|
||||
textureNameNode.name = "TextureName";
|
||||
QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka";
|
||||
auto prop0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
textureNameNode.properties = { prop0 };
|
||||
|
||||
FBXNode relativeFilenameNode;
|
||||
relativeFilenameNode.name = "RelativeFilename";
|
||||
QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename;
|
||||
getTextureContentTypeCallback textureContentTypeCallback = [=]() {
|
||||
QPair<QByteArray, image::TextureUsage::Type> result;
|
||||
result.first = NULL;
|
||||
result.second = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE;
|
||||
return result;
|
||||
};
|
||||
QByteArray* textureFile = this->compressTexture(textureFileName, _objURL, _bakedOutputDir, _textureThreadGetter, textureContentTypeCallback,_originalOutputDir);
|
||||
QVariant textureProperty0;
|
||||
textureProperty0 = QVariant::fromValue(QByteArray(textureFile->data(), (int)textureFile->size()));
|
||||
relativeFilenameNode.properties = { textureProperty0 };
|
||||
|
||||
FBXNode properties70Node;
|
||||
properties70Node.name = "Properties70";
|
||||
|
||||
QVariant texProperty0;
|
||||
QVariant texProperty1;
|
||||
QVariant texProperty2;
|
||||
QVariant texProperty3;
|
||||
QVariant texProperty4;
|
||||
|
||||
double value;
|
||||
|
||||
// Set UseMaterial
|
||||
FBXNode pUseMaterial;
|
||||
pUseMaterial.name = "P";
|
||||
propertyString = "UseMaterial";
|
||||
texProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "bool";
|
||||
texProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
|
||||
int texVal = 1;
|
||||
texProperty4 = texVal;
|
||||
|
||||
pUseMaterial.properties = { texProperty0, texProperty1, texProperty2, texProperty3, texProperty4 };
|
||||
|
||||
FBXNode pUVSet;
|
||||
pUVSet.name = "P";
|
||||
propertyString = "UVSet";
|
||||
texProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "KString";
|
||||
texProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty4 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
|
||||
pUVSet.properties = { texProperty0, texProperty1, texProperty2, texProperty3, texProperty4 };
|
||||
|
||||
FBXNode pUseMipMap;
|
||||
pUseMipMap.name = "P";
|
||||
propertyString = "UseMipMap";
|
||||
texProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "bool";
|
||||
texProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
texProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
|
||||
texVal = 1;
|
||||
texProperty4 = texVal;
|
||||
|
||||
pUseMipMap.properties = { texProperty0, texProperty1, texProperty2, texProperty3, texProperty4 };
|
||||
|
||||
properties70Node.children = { pUVSet, pUseMaterial, pUseMipMap };
|
||||
|
||||
textureNode.children = { textureNameNode,relativeFilenameNode, properties70Node };
|
||||
|
||||
_objectNode.children.append(textureNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Generating Connections node
|
||||
FBXNode connectionsNode;
|
||||
connectionsNode.name = "Connections";
|
||||
// connect Geometry -> Model
|
||||
FBXNode cNode1;
|
||||
cNode1.name = "C";
|
||||
QByteArray propertyString("OO");
|
||||
QVariant property0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
qlonglong childID = _geometryID;
|
||||
QVariant property1(childID);
|
||||
qlonglong parentID = _modelID;
|
||||
QVariant property2(parentID);
|
||||
cNode1.properties = { property0, property1, property2 };
|
||||
connectionsNode.children = { cNode1};
|
||||
|
||||
// connect materials to model
|
||||
for (int i = 0;i < geometry->materials.size();i++) {
|
||||
FBXNode cNode;
|
||||
cNode.name = "C";
|
||||
property0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
property1 = _materialIDs[i];
|
||||
property2 = _modelID;
|
||||
cNode.properties = { property0, property1, property2 };
|
||||
connectionsNode.children.append(cNode);
|
||||
}
|
||||
|
||||
// Texture to material
|
||||
for (int i = 0;i < mapTextureMaterial.size();i++) {
|
||||
FBXNode cNode2;
|
||||
cNode2.name = "C";
|
||||
QByteArray propertyString1("OP");
|
||||
property0 = QVariant::fromValue(QByteArray(propertyString1.data(), (int)propertyString1.size()));
|
||||
property1 = mapTextureMaterial[i].first;
|
||||
int matID = mapTextureMaterial[i].second;
|
||||
property2 = _materialIDs[matID];
|
||||
propertyString1 = "AmbientFactor";
|
||||
QVariant connectionProperty = QVariant::fromValue(QByteArray(propertyString1.data(), (int)propertyString1.size()));
|
||||
cNode2.properties = { property0, property1, property2, connectionProperty };
|
||||
connectionsNode.children.append(cNode2);
|
||||
|
||||
FBXNode cNode4;
|
||||
cNode4.name = "C";
|
||||
propertyString1 = "OP";
|
||||
property0 = QVariant::fromValue(QByteArray(propertyString1.data(), (int)propertyString1.size()));
|
||||
property1 = mapTextureMaterial[i].first;
|
||||
property2 = _materialIDs[matID];
|
||||
propertyString1 = "DiffuseColor";
|
||||
connectionProperty = QVariant::fromValue(QByteArray(propertyString1.data(), (int)propertyString1.size()));
|
||||
cNode4.properties = { property0, property1, property2, connectionProperty };
|
||||
connectionsNode.children.append(cNode4);
|
||||
}
|
||||
|
||||
|
||||
// Connect all generated nodes to rootNode
|
||||
objRoot->children = { globalSettingsNode, _objectNode, connectionsNode };
|
||||
}
|
||||
|
||||
void OBJBaker::setProperties(FBXNode* parentNode) {
|
||||
if (parentNode->name == "P") {
|
||||
QByteArray propertyString("UnitScaleFactor");
|
||||
QVariant property0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "double";
|
||||
QVariant property1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Number";
|
||||
QVariant property2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
QVariant property3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
double unitScaleFactor = 100;
|
||||
QVariant property4(unitScaleFactor);
|
||||
|
||||
parentNode->properties = { property0, property1, property2, property3, property4 };
|
||||
} else if (parentNode->name == "Geometry") {
|
||||
_geometryID = _nodeID;
|
||||
QVariant property0(_nodeID++);
|
||||
QByteArray propertyString("Geometry");
|
||||
QVariant property1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Mesh";
|
||||
QVariant property2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
|
||||
parentNode->properties = { property0, property1, property2 };
|
||||
} else if (parentNode->name == "Model") {
|
||||
_modelID = _nodeID;
|
||||
QVariant property0(_nodeID++);
|
||||
QByteArray propertyString("Model");
|
||||
QVariant property1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Mesh";
|
||||
QVariant property2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
|
||||
parentNode->properties = { property0, property1, property2 };
|
||||
}
|
||||
}
|
||||
|
||||
void OBJBaker::setMaterialNodeProperties(FBXNode* materialNode, QString material, FBXGeometry* geometry) {
|
||||
// Set materialNode properties
|
||||
_materialIDs.push_back(_nodeID);
|
||||
QVariant property0(_nodeID++);
|
||||
QByteArray propertyString(material.toLatin1());
|
||||
QVariant property1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Mesh";
|
||||
QVariant property2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
|
||||
materialNode->properties = { property0, property1, property2 };
|
||||
|
||||
FBXMaterial currentMaterial = geometry->materials[material];
|
||||
|
||||
FBXNode properties70Node;
|
||||
properties70Node.name = "Properties70";
|
||||
|
||||
QVariant materialProperty0;
|
||||
QVariant materialProperty1;
|
||||
QVariant materialProperty2;
|
||||
QVariant materialProperty3;
|
||||
QVariant materialProperty4;
|
||||
QVariant materialProperty5;
|
||||
QVariant materialProperty6;
|
||||
|
||||
double value;
|
||||
|
||||
// Set diffuseColor
|
||||
FBXNode pNodeDiffuseColor;
|
||||
pNodeDiffuseColor.name = "P";
|
||||
propertyString = "DiffuseColor";
|
||||
materialProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Color";
|
||||
materialProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
materialProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "A";
|
||||
materialProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
value = (double)currentMaterial.diffuseColor[0];
|
||||
materialProperty4 = value;
|
||||
value = (double)currentMaterial.diffuseColor[1];
|
||||
materialProperty5 = value;
|
||||
value = (double)currentMaterial.diffuseColor[2];
|
||||
materialProperty6 = value;
|
||||
|
||||
pNodeDiffuseColor.properties = { materialProperty0, materialProperty1, materialProperty2, materialProperty3, materialProperty4, materialProperty5, materialProperty6 };
|
||||
properties70Node.children.append(pNodeDiffuseColor);
|
||||
|
||||
// Set specularColor
|
||||
FBXNode pNodeSpecularColor;
|
||||
pNodeSpecularColor.name = "P";
|
||||
propertyString = "SpecularColor";
|
||||
materialProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Color";
|
||||
materialProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
materialProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "A";
|
||||
materialProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
value = (double)currentMaterial.specularColor[0];
|
||||
materialProperty4 = value;
|
||||
value = (double)currentMaterial.specularColor[1];
|
||||
materialProperty5 = value;
|
||||
value = (double)currentMaterial.specularColor[2];
|
||||
materialProperty6 = value;
|
||||
|
||||
pNodeSpecularColor.properties = { materialProperty0, materialProperty1, materialProperty2, materialProperty3, materialProperty4, materialProperty5, materialProperty6 };
|
||||
properties70Node.children.append(pNodeSpecularColor);
|
||||
|
||||
// Set Shininess
|
||||
FBXNode pNodeShininess;
|
||||
pNodeShininess.name = "P";
|
||||
propertyString = "Shininess";
|
||||
materialProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Number";
|
||||
materialProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
materialProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "A";
|
||||
materialProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
value = (double)currentMaterial.shininess;
|
||||
materialProperty4 = value;
|
||||
|
||||
pNodeShininess.properties = { materialProperty0, materialProperty1, materialProperty2, materialProperty3, materialProperty4 };
|
||||
properties70Node.children.append(pNodeShininess);
|
||||
|
||||
// Set Opacity
|
||||
FBXNode pNodeOpacity;
|
||||
pNodeOpacity.name = "P";
|
||||
propertyString = "Opacity";
|
||||
materialProperty0 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "Number";
|
||||
materialProperty1 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "";
|
||||
materialProperty2 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
propertyString = "A";
|
||||
materialProperty3 = QVariant::fromValue(QByteArray(propertyString.data(), (int)propertyString.size()));
|
||||
value = (double)currentMaterial.opacity;
|
||||
materialProperty4 = value;
|
||||
|
||||
pNodeOpacity.properties = { materialProperty0, materialProperty1, materialProperty2, materialProperty3, materialProperty4 };
|
||||
properties70Node.children.append(pNodeOpacity);
|
||||
|
||||
materialNode->children.append(properties70Node);
|
||||
}
|
63
libraries/baking/src/OBJBaker.h
Normal file
63
libraries/baking/src/OBJBaker.h
Normal file
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// 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<QThread*()>;
|
||||
|
||||
class OBJBaker : public ModelBaker {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
OBJBaker(const QUrl& objURL, TextureBakerThreadGetter textureThreadGetter,
|
||||
const QString& bakedOutputDir, const QString& originalOutputDir = "");
|
||||
~OBJBaker() override;
|
||||
void loadOBJ();
|
||||
void createFBXNodeTree(FBXNode* objRoot, FBXGeometry* geometry);
|
||||
void setProperties(FBXNode * parentNode);
|
||||
void setMaterialNodeProperties(FBXNode* materialNode, QString material, FBXGeometry* geometry);
|
||||
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
|
||||
signals:
|
||||
void OBJLoaded();
|
||||
|
||||
private slots:
|
||||
void startBake();
|
||||
void handleOBJNetworkReply();
|
||||
|
||||
private:
|
||||
qlonglong _nodeID = 0;
|
||||
QUrl _objURL;
|
||||
QString _bakedOBJFilePath;
|
||||
QString _bakedOutputDir;
|
||||
QString _originalOutputDir;
|
||||
QDir _tempDir;
|
||||
QString _originalOBJFilePath;
|
||||
TextureBakerThreadGetter _textureThreadGetter;
|
||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||
|
||||
qlonglong _geometryID;
|
||||
qlonglong _modelID;
|
||||
std::vector<qlonglong> _materialIDs;
|
||||
qlonglong _textureID;
|
||||
std::vector<QPair<qlonglong, int>> mapTextureMaterial;
|
||||
FBXNode _objectNode;
|
||||
};
|
||||
#endif // hifi_OBJBaker_h
|
|
@ -20,6 +20,13 @@
|
|||
#include "Oven.h"
|
||||
#include "BakerCLI.h"
|
||||
|
||||
#include <DependencyManager.h>
|
||||
#include <ResourceManager.h>
|
||||
#include <NodeList.h>
|
||||
#include <AddressManager.h>
|
||||
#include <StatTracker.h>
|
||||
|
||||
|
||||
static const QString OUTPUT_FOLDER = "/Users/birarda/code/hifi/lod/test-oven/export";
|
||||
|
||||
static const QString CLI_INPUT_PARAMETER = "i";
|
||||
|
@ -31,6 +38,12 @@ Oven::Oven(int argc, char* argv[]) :
|
|||
QCoreApplication::setOrganizationName("High Fidelity");
|
||||
QCoreApplication::setApplicationName("Oven");
|
||||
|
||||
// Initialize classes from Dependency Manager for OBJ Baker
|
||||
DependencyManager::set<StatTracker>();
|
||||
DependencyManager::set<AddressManager>();
|
||||
DependencyManager::set<NodeList>(NodeType::Unassigned, -1);
|
||||
DependencyManager::set<ResourceManager>();
|
||||
|
||||
// init the settings interface so we can save and load settings
|
||||
Setting::init();
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
|
||||
#include "../Oven.h"
|
||||
#include "OvenMainWindow.h"
|
||||
|
||||
#include "OBJBaker.h"
|
||||
#include "ModelBakeWidget.h"
|
||||
|
||||
static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory";
|
||||
|
@ -32,8 +32,7 @@ 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();
|
||||
}
|
||||
|
||||
|
@ -114,7 +113,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
|
||||
|
@ -165,7 +164,7 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
|
||||
// split the list from the model line edit to see how many models we need to bake
|
||||
auto fileURLStrings = _modelLineEdit->text().split(',');
|
||||
foreach (QString fileURLString, fileURLStrings) {
|
||||
foreach(QString fileURLString, fileURLStrings) {
|
||||
// construct a URL from the path in the model file text box
|
||||
QUrl modelToBakeURL(fileURLString);
|
||||
|
||||
|
@ -201,25 +200,35 @@ 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<FBXBaker> {
|
||||
new FBXBaker(modelToBakeURL, []() -> QThread* {
|
||||
if (modelToBakeURL.fileName().endsWith(".fbx")) {
|
||||
_baker = std::unique_ptr<FBXBaker>{
|
||||
new FBXBaker(modelToBakeURL, []() -> QThread* {
|
||||
return qApp->getNextWorkerThread();
|
||||
}, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath())
|
||||
};
|
||||
}, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath())
|
||||
};
|
||||
_isFBX = true;
|
||||
} else if (modelToBakeURL.fileName().endsWith(".obj")) {
|
||||
_baker = std::unique_ptr<OBJBaker>{
|
||||
new OBJBaker(modelToBakeURL, []() -> QThread* {
|
||||
return qApp->getNextWorkerThread();
|
||||
}, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath())
|
||||
};
|
||||
_isOBJ = true;
|
||||
}
|
||||
|
||||
// move the baker to the FBX baker thread
|
||||
baker->moveToThread(qApp->getNextWorkerThread());
|
||||
// move the baker to the FBX/OBJ baker thread
|
||||
_baker->moveToThread(qApp->getNextWorkerThread());
|
||||
|
||||
// invoke the bake method on the baker thread
|
||||
QMetaObject::invokeMethod(baker.get(), "bake");
|
||||
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 = qApp->getMainWindow()->showResultsWindow();
|
||||
|
@ -227,32 +236,37 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
|
||||
// keep a unique_ptr to this baker
|
||||
// and remember the row that represents it in the results table
|
||||
_bakers.emplace_back(std::move(baker), resultsRow);
|
||||
_bakers.emplace_back(std::move(_baker), resultsRow);
|
||||
}
|
||||
}
|
||||
|
||||
void ModelBakeWidget::handleFinishedBaker() {
|
||||
if (auto baker = qobject_cast<FBXBaker*>(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;
|
||||
});
|
||||
|
||||
for (auto& file : baker->getOutputFiles()) {
|
||||
qDebug() << "Baked file: " << file;
|
||||
}
|
||||
|
||||
if (it != _bakers.end()) {
|
||||
auto resultRow = it->second;
|
||||
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
|
||||
|
||||
if (baker->hasErrors()) {
|
||||
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
|
||||
} else {
|
||||
resultsWindow->changeStatusForRow(resultRow, "Success");
|
||||
}
|
||||
|
||||
_bakers.erase(it);
|
||||
}
|
||||
Baker* baker;
|
||||
if (_isFBX) {
|
||||
baker = qobject_cast<FBXBaker*>(sender());
|
||||
} else if (_isOBJ) {
|
||||
baker = qobject_cast<OBJBaker*>(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;
|
||||
});
|
||||
|
||||
for (auto& file : baker->getOutputFiles()) {
|
||||
qDebug() << "Baked file: " << file;
|
||||
}
|
||||
|
||||
if (it != _bakers.end()) {
|
||||
auto resultRow = it->second;
|
||||
auto resultsWindow = qApp->getMainWindow()->showResultsWindow();
|
||||
|
||||
if (baker->hasErrors()) {
|
||||
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
|
||||
} else {
|
||||
resultsWindow->changeStatusForRow(resultRow, "Success");
|
||||
}
|
||||
|
||||
_bakers.erase(it);
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
#include <SettingHandle.h>
|
||||
|
||||
#include <FBXBaker.h>
|
||||
#include <OBJBaker.h>
|
||||
|
||||
#include "BakeWidget.h"
|
||||
|
||||
|
@ -46,6 +47,11 @@ private:
|
|||
|
||||
Setting::Handle<QString> _exportDirectory;
|
||||
Setting::Handle<QString> _modelStartDirectory;
|
||||
|
||||
std::unique_ptr<Baker> _baker;
|
||||
|
||||
bool _isOBJ = false;
|
||||
bool _isFBX = false;
|
||||
};
|
||||
|
||||
#endif // hifi_ModelBakeWidget_h
|
||||
#endif // hifi_ModelBakeWidget_h
|
Loading…
Reference in a new issue