Merge pull request #15319 from SamGondelman/materialBaker

Case 21764: Oven bakes model materials directly to separate material description
This commit is contained in:
Sam Gondelman 2019-04-05 17:08:22 -07:00 committed by GitHub
commit 88c278d357
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 314 additions and 693 deletions

View file

@ -33,9 +33,8 @@
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
FBXBaker::FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputModelURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
if (hasBeenBaked) {
// Look for the original model file one directory higher. Perhaps this is an oven output directory.
QUrl originalRelativePath = QUrl("../original/" + inputModelURL.fileName().replace(BAKED_FBX_EXTENSION, FBX_EXTENSION));
@ -45,15 +44,6 @@ FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputText
}
void FBXBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) {
_hfmModel = hfmModel;
if (shouldStop()) {
return;
}
// enumerate the models and textures found in the scene and start a bake for them
rewriteAndBakeSceneTextures();
if (shouldStop()) {
return;
}
@ -114,15 +104,15 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
int meshIndex = 0;
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (FBXNode& object : rootChild.children) {
if (object.name == "Geometry") {
if (object.properties.at(2) == "Mesh") {
for (auto object = rootChild.children.begin(); object != rootChild.children.end(); object++) {
if (object->name == "Geometry") {
if (object->properties.at(2) == "Mesh") {
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
} else if (object.name == "Model") {
for (FBXNode& modelChild : object.children) {
} else if (object->name == "Model") {
for (FBXNode& modelChild : object->children) {
if (modelChild.name == "Properties60" || modelChild.name == "Properties70") {
// This is a properties node
// Remove the geometric transform because that has been applied directly to the vertices in FBXSerializer
@ -142,10 +132,13 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
} else if (modelChild.name == "Vertices") {
// This model is also a mesh
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
replaceMeshNodeWithDraco(*object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
}
} else if (object->name == "Texture" || object->name == "Video") {
// this is an embedded texture, we need to remove it from the FBX
object = rootChild.children.erase(object);
}
if (hasErrors()) {
@ -154,82 +147,4 @@ void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const
}
}
}
}
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 : _hfmModel->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 hfmTextureFileName { 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];
// Compress the texture information and return the new filename to be added into the FBX scene
auto bakedTextureFile = compressTexture(hfmTextureFileName, textureType);
// 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;
}
}
}
}
++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;
}
}
}
}
}
}

View file

@ -31,20 +31,14 @@ using TextureBakerThreadGetter = std::function<QThread*()>;
class FBXBaker : public ModelBaker {
Q_OBJECT
public:
FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
FBXBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
protected:
virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) override;
private:
void rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists);
void rewriteAndBakeSceneTextures();
void replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList);
hfm::Model::Pointer _hfmModel;
bool _pendingErrorEmission { false };
};
#endif // hifi_FBXBaker_h

View file

@ -27,21 +27,11 @@ std::function<QThread*()> MaterialBaker::_getNextOvenWorkerThreadOperator;
static int materialNum = 0;
namespace std {
template <>
struct hash<graphics::Material::MapChannel> {
size_t operator()(const graphics::Material::MapChannel& a) const {
return std::hash<size_t>()((size_t)a);
}
};
};
MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath) :
MaterialBaker::MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir) :
_materialData(materialData),
_isURL(isURL),
_bakedOutputDir(bakedOutputDir),
_textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)),
_destinationPath(destinationPath)
_textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++))
{
}
@ -64,6 +54,14 @@ void MaterialBaker::bake() {
}
}
void MaterialBaker::abort() {
Baker::abort();
for (auto& textureBaker : _textureBakers) {
textureBaker->abort();
}
}
void MaterialBaker::loadMaterial() {
if (!_isURL) {
qCDebug(material_baking) << "Loading local material" << _materialData;
@ -104,45 +102,42 @@ void MaterialBaker::processMaterial() {
for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) {
if (networkMaterial.second) {
auto textureMaps = networkMaterial.second->getTextureMaps();
for (auto textureMap : textureMaps) {
if (textureMap.second && textureMap.second->getTextureSource()) {
graphics::Material::MapChannel mapChannel = textureMap.first;
auto texture = textureMap.second->getTextureSource();
auto textures = networkMaterial.second->getTextures();
for (auto texturePair : textures) {
auto mapChannel = texturePair.first;
auto textureMap = texturePair.second;
if (textureMap.texture && textureMap.texture->_textureSource) {
auto type = textureMap.texture->getTextureType();
QUrl url = texture->getUrl();
QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString();
QByteArray content;
QUrl textureURL;
{
bool foundEmbeddedTexture = false;
auto textureContentMapIter = _textureContentMap.find(networkMaterial.second->getName());
if (textureContentMapIter != _textureContentMap.end()) {
auto textureUsageIter = textureContentMapIter->second.find(type);
if (textureUsageIter != textureContentMapIter->second.end()) {
content = textureUsageIter->second.first;
textureURL = textureUsageIter->second.second;
foundEmbeddedTexture = true;
}
}
if (!foundEmbeddedTexture && textureMap.texture->_textureSource) {
textureURL = textureMap.texture->_textureSource->getUrl().adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
}
}
QString cleanURL = textureURL.toDisplayString();
auto idx = cleanURL.lastIndexOf('.');
auto extension = idx >= 0 ? url.toDisplayString().mid(idx + 1).toLower() : "";
QString extension = idx >= 0 ? cleanURL.mid(idx + 1).toLower() : "";
if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) {
QUrl textureURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// FIXME: this isn't properly handling bumpMaps or glossMaps
static std::unordered_map<graphics::Material::MapChannel, image::TextureUsage::Type> MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP;
if (MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.empty()) {
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::EMISSIVE_MAP] = image::TextureUsage::EMISSIVE_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ALBEDO_MAP] = image::TextureUsage::ALBEDO_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::METALLIC_MAP] = image::TextureUsage::METALLIC_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::ROUGHNESS_MAP] = image::TextureUsage::ROUGHNESS_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::NORMAL_MAP] = image::TextureUsage::NORMAL_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::OCCLUSION_MAP] = image::TextureUsage::OCCLUSION_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::LIGHTMAP_MAP] = image::TextureUsage::LIGHTMAP_TEXTURE;
MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP[graphics::Material::MapChannel::SCATTERING_MAP] = image::TextureUsage::SCATTERING_TEXTURE;
}
auto it = MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.find(mapChannel);
if (it == MAP_CHANNEL_TO_TEXTURE_USAGE_TYPE_MAP.end()) {
handleError("Unknown map channel");
return;
}
QPair<QUrl, image::TextureUsage::Type> textureKey(textureURL, it->second);
QPair<QUrl, image::TextureUsage::Type> textureKey(textureURL, type);
if (!_textureBakers.contains(textureKey)) {
auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second);
auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), type);
QSharedPointer<TextureBaker> textureBaker {
new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName),
new TextureBaker(textureURL, type, _textureOutputDir, "", baseTextureFileName, content),
&TextureBaker::deleteLater
};
textureBaker->setMapChannel(mapChannel);
@ -179,7 +174,7 @@ void MaterialBaker::handleFinishedTextureBaker() {
// Replace the old texture URLs
for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) {
networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL));
networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(relativeURL);
}
} else {
// this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from
@ -245,3 +240,30 @@ void MaterialBaker::outputMaterial() {
// emit signal to indicate the material baking is finished
emit finished();
}
void MaterialBaker::addTexture(const QString& materialName, image::TextureUsage::Type textureUsage, const hfm::Texture& texture) {
auto& textureUsageMap = _textureContentMap[materialName.toStdString()];
if (textureUsageMap.find(textureUsage) == textureUsageMap.end() && !texture.content.isEmpty()) {
textureUsageMap[textureUsage] = { texture.content, texture.filename };
}
};
void MaterialBaker::setMaterials(const QHash<QString, hfm::Material>& materials, const QString& baseURL) {
_materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
for (auto& material : materials) {
_materialResource->parsedMaterials.names.push_back(material.name.toStdString());
_materialResource->parsedMaterials.networkMaterials[material.name.toStdString()] = std::make_shared<NetworkMaterial>(material, baseURL);
// Store any embedded texture content
addTexture(material.name, image::TextureUsage::NORMAL_TEXTURE, material.normalTexture);
addTexture(material.name, image::TextureUsage::ALBEDO_TEXTURE, material.albedoTexture);
addTexture(material.name, image::TextureUsage::GLOSS_TEXTURE, material.glossTexture);
addTexture(material.name, image::TextureUsage::ROUGHNESS_TEXTURE, material.roughnessTexture);
addTexture(material.name, image::TextureUsage::SPECULAR_TEXTURE, material.specularTexture);
addTexture(material.name, image::TextureUsage::METALLIC_TEXTURE, material.metallicTexture);
addTexture(material.name, image::TextureUsage::EMISSIVE_TEXTURE, material.emissiveTexture);
addTexture(material.name, image::TextureUsage::OCCLUSION_TEXTURE, material.occlusionTexture);
addTexture(material.name, image::TextureUsage::SCATTERING_TEXTURE, material.scatteringTexture);
addTexture(material.name, image::TextureUsage::LIGHTMAP_TEXTURE, material.lightmapTexture);
}
}

View file

@ -24,16 +24,19 @@ static const QString BAKED_MATERIAL_EXTENSION = ".baked.json";
class MaterialBaker : public Baker {
Q_OBJECT
public:
MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir, const QUrl& destinationPath);
MaterialBaker(const QString& materialData, bool isURL, const QString& bakedOutputDir);
QString getMaterialData() const { return _materialData; }
bool isURL() const { return _isURL; }
QString getBakedMaterialData() const { return _bakedMaterialData; }
void setMaterials(const QHash<QString, hfm::Material>& materials, const QString& baseURL);
static void setNextOvenWorkerThreadOperator(std::function<QThread*()> getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; }
public slots:
virtual void bake() override;
virtual void abort() override;
signals:
void originalMaterialLoaded();
@ -57,11 +60,18 @@ private:
QString _bakedOutputDir;
QString _textureOutputDir;
QString _bakedMaterialData;
QUrl _destinationPath;
QScriptEngine _scriptEngine;
static std::function<QThread*()> _getNextOvenWorkerThreadOperator;
TextureFileNamer _textureFileNamer;
void addTexture(const QString& materialName, image::TextureUsage::Type textureUsage, const hfm::Texture& texture);
struct TextureUsageHash {
std::size_t operator()(image::TextureUsage::Type textureUsage) const {
return static_cast<std::size_t>(textureUsage);
}
};
std::unordered_map<std::string, std::unordered_map<image::TextureUsage::Type, std::pair<QByteArray, QString>, TextureUsageHash>> _textureContentMap;
};
#endif // !hifi_MaterialBaker_h

View file

@ -42,12 +42,12 @@
#include "baking/BakerLibrary.h"
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
#include <QJsonArray>
ModelBaker::ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
_modelURL(inputModelURL),
_bakedOutputDir(bakedOutputDirectory),
_originalOutputDir(originalOutputDirectory),
_textureThreadGetter(inputTextureThreadGetter),
_hasBeenBaked(hasBeenBaked)
{
auto bakedFilename = _modelURL.fileName();
@ -209,7 +209,6 @@ void ModelBaker::bakeSourceCopy() {
}
hifi::ByteArray modelData = modelFile.readAll();
hfm::Model::Pointer bakedModel;
std::vector<hifi::ByteArray> dracoMeshes;
std::vector<std::vector<hifi::ByteArray>> dracoMaterialLists; // Material order for per-mesh material lookup used by dracoMeshes
@ -245,40 +244,76 @@ void ModelBaker::bakeSourceCopy() {
// Begin hfm baking
baker.run();
bakedModel = baker.getHFMModel();
_hfmModel = baker.getHFMModel();
dracoMeshes = baker.getDracoMeshes();
dracoMaterialLists = baker.getDracoMaterialLists();
}
// Populate _textureContentMap with path to content mappings, for quick lookup by URL
for (auto materialIt = bakedModel->materials.cbegin(); materialIt != bakedModel->materials.cend(); materialIt++) {
static const auto addTexture = [](QHash<hifi::ByteArray, hifi::ByteArray>& textureContentMap, const hfm::Texture& texture) {
if (!textureContentMap.contains(texture.filename)) {
// Content may be empty, unless the data is inlined
textureContentMap[texture.filename] = texture.content;
}
};
const hfm::Material& material = *materialIt;
addTexture(_textureContentMap, material.normalTexture);
addTexture(_textureContentMap, material.albedoTexture);
addTexture(_textureContentMap, material.opacityTexture);
addTexture(_textureContentMap, material.glossTexture);
addTexture(_textureContentMap, material.roughnessTexture);
addTexture(_textureContentMap, material.specularTexture);
addTexture(_textureContentMap, material.metallicTexture);
addTexture(_textureContentMap, material.emissiveTexture);
addTexture(_textureContentMap, material.occlusionTexture);
addTexture(_textureContentMap, material.scatteringTexture);
addTexture(_textureContentMap, material.lightmapTexture);
}
// Do format-specific baking
bakeProcessedSource(bakedModel, dracoMeshes, dracoMaterialLists);
bakeProcessedSource(_hfmModel, dracoMeshes, dracoMaterialLists);
if (shouldStop()) {
return;
}
if (_hfmModel->materials.size() > 0) {
_materialBaker = QSharedPointer<MaterialBaker>(
new MaterialBaker(_modelURL.fileName(), true, _bakedOutputDir),
&MaterialBaker::deleteLater
);
_materialBaker->setMaterials(_hfmModel->materials, _modelURL.toString());
connect(_materialBaker.data(), &MaterialBaker::finished, this, &ModelBaker::handleFinishedMaterialBaker);
_materialBaker->bake();
} else {
outputBakedFST();
}
}
void ModelBaker::handleFinishedMaterialBaker() {
auto baker = qobject_cast<MaterialBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this MaterialBaker is done and everything went according to plan
qCDebug(model_baking) << "Adding baked material to FST mapping " << baker->getBakedMaterialData();
QString relativeBakedMaterialURL = _modelURL.fileName();
auto baseName = relativeBakedMaterialURL.left(relativeBakedMaterialURL.lastIndexOf('.'));
relativeBakedMaterialURL = baseName + BAKED_MATERIAL_EXTENSION;
// First we add the materials in the model
QJsonArray materialMapping;
for (auto material : _hfmModel->materials) {
QJsonObject json;
json["mat::" + material.name] = relativeBakedMaterialURL + "?" + material.name;
materialMapping.push_back(json);
}
// The we add any existing mappings from the mapping
if (_mapping.contains(MATERIAL_MAPPING_FIELD)) {
QByteArray materialMapValue = _mapping[MATERIAL_MAPPING_FIELD].toByteArray();
QJsonObject oldMaterialMapping = QJsonDocument::fromJson(materialMapValue).object();
for (auto key : oldMaterialMapping.keys()) {
QJsonObject json;
json[key] = oldMaterialMapping[key];
materialMapping.push_back(json);
}
}
_mapping[MATERIAL_MAPPING_FIELD] = QJsonDocument(materialMapping).toJson(QJsonDocument::Compact);
} else {
// this material failed to bake - this doesn't fail the entire bake but we need to add the errors from
// the material to our warnings
_warningList << baker->getWarnings();
}
} else {
handleWarning("Failed to bake the materials for model with URL " + _modelURL.toString());
}
outputBakedFST();
}
void ModelBaker::outputBakedFST() {
// Output FST file, copying over input mappings if available
QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName();
auto extensionStart = outputFSTFilename.indexOf(".");
@ -291,8 +326,7 @@ void ModelBaker::bakeSourceCopy() {
auto outputMapping = _mapping;
outputMapping[FST_VERSION_FIELD] = FST_VERSION;
outputMapping[FILENAME_FIELD] = _bakedModelURL.fileName();
// All textures will be found in the same directory as the model
outputMapping[TEXDIR_FIELD] = ".";
outputMapping.remove(TEXDIR_FIELD);
hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping);
QFile fstOutputFile { outputFSTURL };
@ -307,17 +341,16 @@ void ModelBaker::bakeSourceCopy() {
_outputFiles.push_back(outputFSTURL);
_outputMappingURL = outputFSTURL;
// check if we're already done with textures (in case we had none to re-write)
checkIfTexturesFinished();
exportScene();
qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL;
emit finished();
}
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();
if (_materialBaker) {
_materialBaker->abort();
}
}
@ -354,247 +387,6 @@ bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dr
return true;
}
QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) {
QFileInfo modelTextureFileInfo { modelTextureFileName.replace("\\", "/") };
if (modelTextureFileInfo.suffix().toLower() == BAKED_TEXTURE_KTX_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, !textureContent.isNull());
TextureKey textureKey { urlToTexture, textureType };
auto bakingTextureIt = _bakingTextures.find(textureKey);
if (bakingTextureIt == _bakingTextures.cend()) {
// 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
QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType);
QString bakedTextureFilePath {
_bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX
};
textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX;
_outputFiles.push_back(bakedTextureFilePath);
// bake this texture asynchronously
bakeTexture(textureKey, _bakedOutputDir, baseTextureFileName, textureContent);
} else {
// Fetch existing texture meta name
textureChild = (*bakingTextureIt)->getBaseFilename() + BAKED_META_TEXTURE_SUFFIX;
}
}
qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName
<< "to" << textureChild;
return textureChild;
}
void ModelBaker::bakeTexture(const TextureKey& textureKey, 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(textureKey.first, textureKey.second, 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(textureKey, 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());
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(), bakedTexture->getTextureType() });
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(), bakedTexture->getTextureType() });
// 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(), bakedTexture->getTextureType() });
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<TextureBaker*>(sender());
qDebug() << "Texture aborted: " << bakedTexture->getTextureURL();
if (bakedTexture) {
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
}
// 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, bool isEmbedded) {
QUrl urlToTexture;
if (isEmbedded) {
urlToTexture = _modelURL.toString() + "/" + textureFileInfo.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() && textureFileInfo.exists() && textureFileInfo.isFile()) {
// the absolute path we ran into for the texture in the model exists on this machine
// so use that file
urlToTexture = QUrl::fromLocalFile(textureFileInfo.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(textureFileInfo.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;
texturesFinished();
setIsFinished(true);
}
}
}
void ModelBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
@ -605,70 +397,6 @@ void ModelBaker::setWasAborted(bool wasAborted) {
}
}
void ModelBaker::texturesFinished() {
embedTextureMetaData();
exportScene();
}
void ModelBaker::embedTextureMetaData() {
std::vector<FBXNode> embeddedTextureNodes;
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
qlonglong maxId = 0;
for (auto &child : rootChild.children) {
if (child.properties.length() == 3) {
maxId = std::max(maxId, child.properties[0].toLongLong());
}
}
for (auto& object : rootChild.children) {
if (object.name == "Texture") {
QVariant relativeFilename;
for (auto& child : object.children) {
if (child.name == "RelativeFilename") {
relativeFilename = child.properties[0];
break;
}
}
if (relativeFilename.isNull()
|| !relativeFilename.toString().endsWith(BAKED_META_TEXTURE_SUFFIX)) {
continue;
}
if (object.properties.length() < 2) {
qWarning() << "Found texture with unexpected number of properties: " << object.name;
continue;
}
FBXNode videoNode;
videoNode.name = "Video";
videoNode.properties.append(++maxId);
videoNode.properties.append(object.properties[1]);
videoNode.properties.append("Clip");
QString bakedTextureFilePath {
_bakedOutputDir + "/" + relativeFilename.toString()
};
QFile textureFile { bakedTextureFilePath };
if (!textureFile.open(QIODevice::ReadOnly)) {
qWarning() << "Failed to open: " << bakedTextureFilePath;
continue;
}
videoNode.children.append({ "RelativeFilename", { relativeFilename }, { } });
videoNode.children.append({ "Content", { textureFile.readAll() }, { } });
rootChild.children.append(videoNode);
textureFile.close();
}
}
}
}
}
void ModelBaker::exportScene() {
auto fbxData = FBXWriter::encodeFBX(_rootNode);

View file

@ -18,17 +18,13 @@
#include <QtNetwork/QNetworkReply>
#include "Baker.h"
#include "TextureBaker.h"
#include "baking/TextureFileNamer.h"
#include "MaterialBaker.h"
#include "ModelBakingLoggingCategory.h"
#include <gpu/Texture.h>
#include <FBX.h>
#include <hfm/HFM.h>
using TextureBakerThreadGetter = std::function<QThread*()>;
using GetMaterialIDCallback = std::function <int(int)>;
static const QString FST_EXTENSION { ".fst" };
@ -42,10 +38,7 @@ class ModelBaker : public Baker {
Q_OBJECT
public:
using TextureKey = QPair<QUrl, image::TextureUsage::Type>;
ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
ModelBaker(const QUrl& inputModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
void setOutputURLSuffix(const QUrl& urlSuffix);
void setMappingURL(const QUrl& mappingURL);
@ -54,7 +47,6 @@ public:
void initializeOutputDirs();
bool buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList);
QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE);
virtual void setWasAborted(bool wasAborted) override;
QUrl getModelURL() const { return _modelURL; }
@ -71,20 +63,15 @@ public slots:
protected:
void saveSourceModel();
virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) = 0;
void checkIfTexturesFinished();
void texturesFinished();
void embedTextureMetaData();
void exportScene();
FBXNode _rootNode;
QHash<QByteArray, QByteArray> _textureContentMap;
QUrl _modelURL;
QUrl _outputURLSuffix;
QUrl _mappingURL;
hifi::VariantHash _mapping;
QString _bakedOutputDir;
QString _originalOutputDir;
TextureBakerThreadGetter _textureThreadGetter;
QString _originalOutputModelPath;
QString _outputMappingURL;
QUrl _bakedModelURL;
@ -92,23 +79,15 @@ protected:
protected slots:
void handleModelNetworkReply();
virtual void bakeSourceCopy();
private slots:
void handleBakedTexture();
void handleAbortedTexture();
void handleFinishedMaterialBaker();
private:
QUrl getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded = false);
void bakeTexture(const TextureKey& textureKey, const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent);
QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL);
QMultiHash<TextureKey, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
bool _pendingErrorEmission { false };
void outputBakedFST();
bool _hasBeenBaked { false };
TextureFileNamer _textureFileNamer;
hfm::Model::Pointer _hfmModel;
QSharedPointer<MaterialBaker> _materialBaker;
};
#endif // hifi_ModelBaker_h

View file

@ -132,55 +132,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h
handleWarning("Baked mesh for OBJ model '" + _modelURL.toString() + "' is empty");
}
// 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;
HFMMaterial currentMaterial = hfmModel->materials[material];
if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) {
auto textureID = nextNodeID();
_mapTextureMaterial.emplace_back(textureID, i);
FBXNode textureNode;
{
textureNode.name = TEXTURE_NODE_NAME;
textureNode.properties = { textureID, "texture" + QString::number(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
connectionsNode.name = CONNECTIONS_NODE_NAME;
@ -199,29 +150,6 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& h
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);
}
}
// Set properties for material nodes

View file

@ -35,9 +35,7 @@ private:
void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel);
NodeID nextNodeID() { return _nodeID++; }
NodeID _nodeID { 0 };
std::vector<NodeID> _materialIDs;
std::vector<std::pair<NodeID, int>> _mapTextureMaterial;
};
#endif // hifi_OBJBaker_h

View file

@ -44,7 +44,7 @@ bool isModelBaked(const QUrl& bakeableModelURL) {
return beforeModelExtension.endsWith(".baked");
}
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath) {
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath) {
auto filename = bakeableModelURL.fileName();
// Output in a sub-folder with the name of the model, potentially suffixed by a number to make it unique
@ -58,20 +58,20 @@ std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureB
QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked";
QString originalOutputDirectory = contentOutputPath + subDirName + "/original";
return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
return getModelBakerWithOutputDirectories(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory);
}
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) {
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory) {
auto filename = bakeableModelURL.fileName();
std::unique_ptr<ModelBaker> baker;
if (filename.endsWith(FST_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<FSTBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive));
baker = std::make_unique<FSTBaker>(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FST_EXTENSION, Qt::CaseInsensitive));
} else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<FBXBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive));
baker = std::make_unique<FBXBaker>(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory, filename.endsWith(BAKED_FBX_EXTENSION, Qt::CaseInsensitive));
} else if (filename.endsWith(OBJ_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<OBJBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
baker = std::make_unique<OBJBaker>(bakeableModelURL, bakedOutputDirectory, originalOutputDirectory);
//} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) {
//baker = std::make_unique<GLTFBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
} else {

View file

@ -23,9 +23,9 @@ bool isModelBaked(const QUrl& bakeableModelURL);
// Assuming the URL is valid, gets the appropriate baker for the given URL, and creates the base directory where the baker's output will later be stored
// Returns an empty pointer if a baker could not be created
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& contentOutputPath);
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, const QString& contentOutputPath);
// Similar to getModelBaker, but gives control over where the output folders will be
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, const QString& bakedOutputDirectory, const QString& originalOutputDirectory);
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory);
#endif // hifi_BakerLibrary_h

View file

@ -18,9 +18,8 @@
#include <FSTReader.h>
FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputMappingURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
FSTBaker::FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputMappingURL, bakedOutputDirectory, originalOutputDirectory, hasBeenBaked) {
if (hasBeenBaked) {
// Look for the original model file one directory higher. Perhaps this is an oven output directory.
QUrl originalRelativePath = QUrl("../original/" + inputMappingURL.fileName().replace(BAKED_FST_EXTENSION, FST_EXTENSION));
@ -70,7 +69,7 @@ void FSTBaker::bakeSourceCopy() {
return;
}
auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _bakedOutputDir, _originalOutputDir);
auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _bakedOutputDir, _originalOutputDir);
_modelBaker = std::unique_ptr<ModelBaker>(dynamic_cast<ModelBaker*>(baker.release()));
if (!_modelBaker) {
handleError("The model url '" + bakeableModelURL.toString() + "' from the FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') could not be used to initialize a valid model baker");

View file

@ -18,8 +18,7 @@ class FSTBaker : public ModelBaker {
Q_OBJECT
public:
FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
FSTBaker(const QUrl& inputMappingURL, const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
virtual QUrl getFullOutputMappingURL() const override;

View file

@ -97,7 +97,7 @@ QString processID(const QString& id) {
return id.mid(id.lastIndexOf(':') + 1);
}
QString getName(const QVariantList& properties) {
QString getModelName(const QVariantList& properties) {
QString name;
if (properties.size() == 3) {
name = properties.at(1).toString();
@ -108,6 +108,17 @@ QString getName(const QVariantList& properties) {
return name;
}
QString getMaterialName(const QVariantList& properties) {
QString name;
if (properties.size() == 1 || properties.at(1).toString().isEmpty()) {
name = properties.at(0).toString();
name = processID(name.left(name.indexOf(QChar('\0'))));
} else {
name = processID(properties.at(1).toString());
}
return name;
}
QString getID(const QVariantList& properties, int index = 0) {
return processID(properties.at(index).toString());
}
@ -508,7 +519,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
blendshapes.append(extracted);
}
} else if (object.name == "Model") {
QString name = getName(object.properties);
QString name = getModelName(object.properties);
QString id = getID(object.properties);
modelIDsToNames.insert(id, name);
@ -827,7 +838,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
} else if (object.name == "Material") {
HFMMaterial material;
MaterialParam materialParam;
material.name = (object.properties.at(1).toString());
material.name = getMaterialName(object.properties);
foreach (const FBXNode& subobject, object.children) {
bool properties = false;

View file

@ -32,6 +32,7 @@ static const QString JOINT_FIELD = "joint";
static const QString BLENDSHAPE_FIELD = "bs";
static const QString SCRIPT_FIELD = "script";
static const QString JOINT_NAME_MAPPING_FIELD = "jointMap";
static const QString MATERIAL_MAPPING_FIELD = "materialMap";
class FSTReader {
public:

View file

@ -891,12 +891,14 @@ HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::V
if (!objMaterial.used) {
continue;
}
hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor,
objMaterial.specularColor,
objMaterial.emissiveColor,
objMaterial.shininess,
objMaterial.opacity);
HFMMaterial& hfmMaterial = hfmModel.materials[materialID];
HFMMaterial& hfmMaterial = hfmModel.materials[materialID] = HFMMaterial(objMaterial.diffuseColor,
objMaterial.specularColor,
objMaterial.emissiveColor,
objMaterial.shininess,
objMaterial.opacity);
hfmMaterial.name = materialID;
hfmMaterial.materialID = materialID;
hfmMaterial._material = std::make_shared<graphics::Material>();
graphics::MaterialPointer modelMaterial = hfmMaterial._material;

View file

@ -559,8 +559,7 @@ void NetworkMaterial::setLightmapMap(const QUrl& url) {
}
NetworkMaterial::NetworkMaterial(const HFMMaterial& material, const QUrl& textureBaseUrl) :
graphics::Material(*material._material),
_textures(MapChannel::NUM_MAP_CHANNELS)
graphics::Material(*material._material)
{
_name = material.name.toStdString();
if (!material.albedoTexture.filename.isEmpty()) {
@ -709,7 +708,7 @@ void NetworkMaterial::setTextures(const QVariantMap& textureMap) {
bool NetworkMaterial::isMissingTexture() {
for (auto& networkTexture : _textures) {
auto& texture = networkTexture.texture;
auto& texture = networkTexture.second.texture;
if (!texture) {
continue;
}

View file

@ -36,15 +36,21 @@ public:
bool isMissingTexture();
void checkResetOpacityMap();
protected:
friend class Geometry;
class Texture {
public:
QString name;
NetworkTexturePointer texture;
};
using Textures = std::vector<Texture>;
struct MapChannelHash {
std::size_t operator()(MapChannel mapChannel) const {
return static_cast<std::size_t>(mapChannel);
}
};
using Textures = std::unordered_map<MapChannel, Texture, MapChannelHash>;
Textures getTextures() { return _textures; }
protected:
friend class Geometry;
Textures _textures;

View file

@ -10,6 +10,62 @@
#include "ModelBakerLogging.h"
#include <QJsonArray>
void processMaterialMapping(MaterialMapping& materialMapping, const QJsonObject& materialMap, const hifi::URL& url) {
auto mappingKeys = materialMap.keys();
for (auto mapping : mappingKeys) {
auto mappingJSON = materialMap[mapping];
if (mappingJSON.isObject()) {
auto mappingValue = mappingJSON.toObject();
// Old subsurface scattering mapping
{
auto scatteringIter = mappingValue.find("scattering");
auto scatteringMapIter = mappingValue.find("scatteringMap");
if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) {
std::shared_ptr<NetworkMaterial> material = std::make_shared<NetworkMaterial>();
if (scatteringIter != mappingValue.end()) {
float scattering = (float)scatteringIter.value().toDouble();
material->setScattering(scattering);
}
if (scatteringMapIter != mappingValue.end()) {
QString scatteringMap = scatteringMapIter.value().toString();
material->setScatteringMap(scatteringMap);
}
material->setDefaultFallthrough(true);
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(),
[](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials.names.push_back("scattering");
materialResource->parsedMaterials.networkMaterials["scattering"] = material;
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>("mat::" + mapping.toStdString(), materialResource));
continue;
}
}
// Material JSON description
{
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(),
[](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource));
}
} else if (mappingJSON.isString()) {
auto mappingValue = mappingJSON.toString();
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(),
MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
}
}
}
void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
const auto& mapping = input.get0();
const auto& url = input.get1();
@ -18,56 +74,18 @@ void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, con
auto mappingIter = mapping.find("materialMap");
if (mappingIter != mapping.end()) {
QByteArray materialMapValue = mappingIter.value().toByteArray();
QJsonObject materialMap = QJsonDocument::fromJson(materialMapValue).object();
if (materialMap.isEmpty()) {
QJsonDocument materialMapJSON = QJsonDocument::fromJson(materialMapValue);
if (materialMapJSON.isEmpty()) {
qCDebug(model_baker) << "Material Map found but did not produce valid JSON:" << materialMapValue;
} else if (materialMapJSON.isObject()) {
QJsonObject materialMap = materialMapJSON.object();
processMaterialMapping(materialMapping, materialMap, url);
} else {
auto mappingKeys = materialMap.keys();
for (auto mapping : mappingKeys) {
auto mappingJSON = materialMap[mapping];
if (mappingJSON.isObject()) {
auto mappingValue = mappingJSON.toObject();
// Old subsurface scattering mapping
{
auto scatteringIter = mappingValue.find("scattering");
auto scatteringMapIter = mappingValue.find("scatteringMap");
if (scatteringIter != mappingValue.end() || scatteringMapIter != mappingValue.end()) {
std::shared_ptr<NetworkMaterial> material = std::make_shared<NetworkMaterial>();
if (scatteringIter != mappingValue.end()) {
float scattering = (float)scatteringIter.value().toDouble();
material->setScattering(scattering);
}
if (scatteringMapIter != mappingValue.end()) {
QString scatteringMap = scatteringMapIter.value().toString();
material->setScatteringMap(scatteringMap);
}
material->setDefaultFallthrough(true);
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials.names.push_back("scattering");
materialResource->parsedMaterials.networkMaterials["scattering"] = material;
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>("mat::" + mapping.toStdString(), materialResource));
continue;
}
}
// Material JSON description
{
NetworkMaterialResourcePointer materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource(), [](NetworkMaterialResource* ptr) { ptr->deleteLater(); });
materialResource->moveToThread(qApp->thread());
materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument(mappingValue), url);
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), materialResource));
}
} else if (mappingJSON.isString()) {
auto mappingValue = mappingJSON.toString();
materialMapping.push_back(std::pair<std::string, NetworkMaterialResourcePointer>(mapping.toStdString(), MaterialCache::instance().getMaterial(url.resolved(mappingValue))));
QJsonArray materialMapArray = materialMapJSON.array();
for (auto materialMapIter : materialMapArray) {
if (materialMapIter.isObject()) {
QJsonObject materialMap = materialMapIter.toObject();
processMaterialMapping(materialMapping, materialMap, url);
}
}
}

View file

@ -437,8 +437,8 @@ const QVariantMap Geometry::getTextures() const {
QVariantMap textures;
for (const auto& material : _materials) {
for (const auto& texture : material->_textures) {
if (texture.texture) {
textures[texture.name] = texture.texture->getURL();
if (texture.second.texture) {
textures[texture.second.name] = texture.second.texture->getURL();
}
}
}
@ -467,7 +467,7 @@ void Geometry::setTextures(const QVariantMap& textureMap) {
for (auto& material : _materials) {
// Check if any material textures actually changed
if (std::any_of(material->_textures.cbegin(), material->_textures.cend(),
[&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.texture && textureMap.contains(it.name); })) {
[&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.second.texture && textureMap.contains(it.second.name); })) {
// FIXME: The Model currently caches the materials (waste of space!)
// so they must be copied in the Geometry copy-ctor

View file

@ -1546,18 +1546,40 @@ void Model::applyMaterialMapping() {
continue;
}
auto materialLoaded = [this, networkMaterialResource, shapeIDs, renderItemsKey, primitiveMode, useDualQuaternionSkinning]() {
// This needs to be precomputed before the lambda, since the lambdas could be called out of order
std::unordered_map<unsigned int, quint16> priorityMapPerResource;
for (auto shapeID : shapeIDs) {
priorityMapPerResource[shapeID] = ++_priorityMap[shapeID];
}
auto materialLoaded = [this, networkMaterialResource, shapeIDs, priorityMapPerResource, renderItemsKey, primitiveMode, useDualQuaternionSkinning]() {
if (networkMaterialResource->isFailed() || networkMaterialResource->parsedMaterials.names.size() == 0) {
return;
}
render::Transaction transaction;
auto networkMaterial = networkMaterialResource->parsedMaterials.networkMaterials[networkMaterialResource->parsedMaterials.names[0]];
std::shared_ptr<NetworkMaterial> networkMaterial;
{
QString url = networkMaterialResource->getURL().toString();
bool foundMaterialName = false;
if (url.contains("?")) {
auto split = url.split("?");
std::string materialName = split.last().toStdString();
auto networkMaterialIter = networkMaterialResource->parsedMaterials.networkMaterials.find(materialName);
if (networkMaterialIter != networkMaterialResource->parsedMaterials.networkMaterials.end()) {
networkMaterial = networkMaterialIter->second;
foundMaterialName = true;
}
}
if (!foundMaterialName) {
networkMaterial = networkMaterialResource->parsedMaterials.networkMaterials[networkMaterialResource->parsedMaterials.names[0]];
}
}
for (auto shapeID : shapeIDs) {
if (shapeID < _modelMeshRenderItemIDs.size()) {
auto itemID = _modelMeshRenderItemIDs[shapeID];
auto meshIndex = _modelMeshRenderItemShapes[shapeID].meshIndex;
bool invalidatePayloadShapeKey = shouldInvalidatePayloadShapeKey(meshIndex);
graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, ++_priorityMap[shapeID]);
graphics::MaterialLayer material = graphics::MaterialLayer(networkMaterial, priorityMapPerResource.at(shapeID));
_materialMapping[shapeID].push_back(material);
transaction.updateItem<ModelMeshPartPayload>(itemID, [material, renderItemsKey,
invalidatePayloadShapeKey, primitiveMode, useDualQuaternionSkinning](ModelMeshPartPayload& data) {

View file

@ -49,10 +49,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString&
if (type == MODEL_EXTENSION || type == FBX_EXTENSION) {
QUrl bakeableModelURL = getBakeableModelURL(inputUrl);
if (!bakeableModelURL.isEmpty()) {
auto getWorkerThreadCallback = []() -> QThread* {
return Oven::instance().getNextWorkerThread();
};
_baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputPath);
_baker = getModelBaker(bakeableModelURL, outputPath);
if (_baker) {
_baker->moveToThread(Oven::instance().getNextWorkerThread());
}
@ -61,7 +58,7 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString&
_baker = std::unique_ptr<Baker> { new JSBaker(inputUrl, outputPath) };
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else if (type == MATERIAL_EXTENSION) {
_baker = std::unique_ptr<Baker> { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath, QUrl(outputPath)) };
_baker = std::unique_ptr<Baker> { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath) };
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else {
// If the type doesn't match the above, we assume we have a texture, and the type specified is the

View file

@ -152,10 +152,7 @@ void DomainBaker::addModelBaker(const QString& property, const QString& url, con
// setup a ModelBaker for this URL, as long as we don't already have one
bool haveBaker = _modelBakers.contains(bakeableModelURL);
if (!haveBaker) {
auto getWorkerThreadCallback = []() -> QThread* {
return Oven::instance().getNextWorkerThread();
};
QSharedPointer<ModelBaker> baker = QSharedPointer<ModelBaker>(getModelBaker(bakeableModelURL, getWorkerThreadCallback, _contentOutputPath).release(), &Baker::deleteLater);
QSharedPointer<ModelBaker> baker = QSharedPointer<ModelBaker>(getModelBaker(bakeableModelURL, _contentOutputPath).release(), &Baker::deleteLater);
if (baker) {
// Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL
// Note: The ModelBaker currently doesn't store this in the FST because the equal signs mess up FST parsing.
@ -275,7 +272,7 @@ void DomainBaker::addMaterialBaker(const QString& property, const QString& data,
// setup a baker for this material
QSharedPointer<MaterialBaker> materialBaker {
new MaterialBaker(data, isURL, _contentOutputPath, _destinationPath),
new MaterialBaker(data, isURL, _contentOutputPath),
&MaterialBaker::deleteLater
};

View file

@ -180,11 +180,7 @@ void ModelBakeWidget::bakeButtonClicked() {
QUrl bakeableModelURL = getBakeableModelURL(modelToBakeURL);
if (!bakeableModelURL.isEmpty()) {
auto getWorkerThreadCallback = []() -> QThread* {
return Oven::instance().getNextWorkerThread();
};
std::unique_ptr<Baker> baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputDirectory.path());
std::unique_ptr<Baker> baker = getModelBaker(bakeableModelURL, outputDirectory.path());
if (baker) {
// everything seems to be in place, kick off a bake for this model now