mirror of
https://thingvellir.net/git/overte
synced 2025-03-27 23:52:03 +01:00
remove all existing material parsing
This commit is contained in:
parent
754b281bbf
commit
3d2b71bc7b
15 changed files with 46 additions and 573 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -64,6 +64,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;
|
||||
|
|
|
@ -34,6 +34,7 @@ public:
|
|||
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
virtual void abort() override;
|
||||
|
||||
signals:
|
||||
void originalMaterialLoaded();
|
||||
|
|
|
@ -42,12 +42,10 @@
|
|||
|
||||
#include "baking/BakerLibrary.h"
|
||||
|
||||
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
|
||||
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
|
||||
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();
|
||||
|
@ -250,28 +248,6 @@ void ModelBaker::bakeSourceCopy() {
|
|||
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);
|
||||
|
||||
|
@ -291,8 +267,6 @@ 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] = ".";
|
||||
hifi::ByteArray fstOut = FSTReader::writeMapping(outputMapping);
|
||||
|
||||
QFile fstOutputFile { outputFSTURL };
|
||||
|
@ -307,18 +281,15 @@ 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();
|
||||
}
|
||||
_materialBaker->abort();
|
||||
}
|
||||
|
||||
bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList) {
|
||||
|
@ -354,247 +325,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 +335,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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
@ -93,22 +80,10 @@ protected slots:
|
|||
void handleModelNetworkReply();
|
||||
virtual void bakeSourceCopy();
|
||||
|
||||
private slots:
|
||||
void handleBakedTexture();
|
||||
void handleAbortedTexture();
|
||||
|
||||
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 };
|
||||
|
||||
bool _hasBeenBaked { false };
|
||||
|
||||
TextureFileNamer _textureFileNamer;
|
||||
QSharedPointer<MaterialBaker> _materialBaker;
|
||||
};
|
||||
|
||||
#endif // hifi_ModelBaker_h
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -45,7 +45,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
|
||||
|
@ -59,20 +59,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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue