Merge pull request #15176 from sabrina-shanman/hfm_oven_wip

(case 21205) Incorporate HFM in Oven + Add FST baking/output + More Oven Improvements
This commit is contained in:
Sam Gateau 2019-03-19 13:03:49 -07:00 committed by GitHub
commit d88bee89e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 2823 additions and 1435 deletions

View file

@ -1,8 +1,6 @@
set(TARGET_NAME baking)
setup_hifi_library(Concurrent)
link_hifi_libraries(shared graphics networking ktx image fbx)
link_hifi_libraries(shared shaders graphics networking material-networking graphics-scripting ktx image fbx model-baker task)
include_hifi_library_headers(gpu)
include_hifi_library_headers(hfm)
target_draco()

View file

@ -52,7 +52,7 @@ protected:
void handleErrors(const QStringList& errors);
// List of baked output files. For instance, for an FBX this would
// include the .fbx and all of its texture files.
// include the .fbx, a .fst pointing to the fbx, and all of the fbx texture files.
std::vector<QString> _outputFiles;
QStringList _errorList;

View file

@ -33,29 +33,19 @@
#include "ModelBakingLoggingCategory.h"
#include "TextureBaker.h"
#ifdef HIFI_DUMP_FBX
#include "FBXToJSON.h"
#endif
void FBXBaker::bake() {
qDebug() << "FBXBaker" << _modelURL << "bake starting";
// setup the output folder for the results of this bake
setupOutputFolder();
if (shouldStop()) {
return;
FBXBaker::FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputModelURL, inputTextureThreadGetter, 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));
QUrl newInputModelURL = inputModelURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath);
_modelURL = newInputModelURL;
}
connect(this, &FBXBaker::sourceCopyReadyToLoad, this, &FBXBaker::bakeSourceCopy);
// make a local copy of the FBX file
loadSourceFBX();
}
void FBXBaker::bakeSourceCopy() {
// load the scene from the FBX file
importScene();
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;
@ -68,222 +58,100 @@ void FBXBaker::bakeSourceCopy() {
return;
}
rewriteAndBakeSceneModels();
rewriteAndBakeSceneModels(hfmModel->meshes, dracoMeshes, dracoMaterialLists);
}
if (shouldStop()) {
void FBXBaker::replaceMeshNodeWithDraco(FBXNode& meshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList) {
// Compress mesh information and store in dracoMeshNode
FBXNode dracoMeshNode;
bool success = buildDracoMeshNode(dracoMeshNode, dracoMeshBytes, dracoMaterialList);
if (!success) {
return;
}
// check if we're already done with textures (in case we had none to re-write)
checkIfTexturesFinished();
}
void FBXBaker::setupOutputFolder() {
// make sure there isn't already an output directory using the same name
if (QDir(_bakedOutputDir).exists()) {
qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing.";
} else {
qCDebug(model_baking) << "Creating FBX output folder" << _bakedOutputDir;
meshNode.children.push_back(dracoMeshNode);
// attempt to make the output folder
if (!QDir().mkpath(_bakedOutputDir)) {
handleError("Failed to create FBX output folder " + _bakedOutputDir);
return;
}
// attempt to make the output folder
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create FBX output folder " + _originalOutputDir);
return;
}
}
}
static const std::vector<QString> nodeNamesToDelete {
// Node data that is packed into the draco mesh
"Vertices",
"PolygonVertexIndex",
"LayerElementNormal",
"LayerElementColor",
"LayerElementUV",
"LayerElementMaterial",
"LayerElementTexture",
void FBXBaker::loadSourceFBX() {
// check if the FBX is local or first needs to be downloaded
if (_modelURL.isLocalFile()) {
// load up the local file
QFile localFBX { _modelURL.toLocalFile() };
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
if (!localFBX.exists()) {
//QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), "");
handleError("Could not find " + _modelURL.toString());
return;
}
// make a copy in the output folder
if (!_originalOutputDir.isEmpty()) {
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
localFBX.copy(_originalModelFilePath);
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
} else {
// remote file, kick off a download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_modelURL);
qCDebug(model_baking) << "Downloading" << _modelURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply);
}
}
void FBXBaker::handleFBXNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _modelURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(_originalModelFilePath);
qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
return;
}
// close that file now that we are done writing to it
copyOfOriginal.close();
if (!_originalOutputDir.isEmpty()) {
copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
// emit our signal to start the import of the FBX source copy
emit sourceCopyReadyToLoad();
} else {
// add an error to our list stating that the FBX could not be downloaded
handleError("Failed to download " + _modelURL.toString());
}
}
void FBXBaker::importScene() {
qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists();
QFile fbxFile(_originalModelFilePath);
if (!fbxFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalModelFilePath + " for reading");
return;
}
FBXSerializer fbxSerializer;
qCDebug(model_baking) << "Parsing" << _modelURL;
_rootNode = fbxSerializer._rootNode = fbxSerializer.parseFBX(&fbxFile);
#ifdef HIFI_DUMP_FBX
{
FBXToJSON fbxToJSON;
fbxToJSON << _rootNode;
QFileInfo modelFile(_originalModelFilePath);
QString outFilename(_bakedOutputDir + "/" + modelFile.completeBaseName() + "_FBX.json");
QFile jsonFile(outFilename);
if (jsonFile.open(QIODevice::WriteOnly)) {
jsonFile.write(fbxToJSON.str().c_str(), fbxToJSON.str().length());
jsonFile.close();
}
}
#endif
_hfmModel = fbxSerializer.extractHFMModel({}, _modelURL.toString());
_textureContentMap = fbxSerializer._textureContent;
}
void FBXBaker::rewriteAndBakeSceneModels() {
unsigned int meshIndex = 0;
bool hasDeformers { false };
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (FBXNode& objectChild : rootChild.children) {
if (objectChild.name == "Deformer") {
hasDeformers = true;
break;
}
// Node data that we don't support
"Edges",
"LayerElementTangent",
"LayerElementBinormal",
"LayerElementSmoothing"
};
auto& children = meshNode.children;
auto it = children.begin();
while (it != children.end()) {
auto begin = nodeNamesToDelete.begin();
auto end = nodeNamesToDelete.end();
if (find(begin, end, it->name) != end) {
it = children.erase(it);
} else {
++it;
}
}
if (hasDeformers) {
break;
}
}
}
void FBXBaker::rewriteAndBakeSceneModels(const QVector<hfm::Mesh>& meshes, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) {
std::vector<int> meshIndexToRuntimeOrder;
auto meshCount = (int)meshes.size();
meshIndexToRuntimeOrder.resize(meshCount);
for (int i = 0; i < meshCount; i++) {
meshIndexToRuntimeOrder[meshes[i].meshIndex] = i;
}
// The meshIndex represents the order in which the meshes are loaded from the FBX file
// We replicate this order by iterating over the meshes in the same way that FBXSerializer does
int meshIndex = 0;
for (FBXNode& rootChild : _rootNode.children) {
if (rootChild.name == "Objects") {
for (FBXNode& objectChild : rootChild.children) {
if (objectChild.name == "Geometry") {
// TODO Pull this out of _hfmModel instead so we don't have to reprocess it
auto extractedMesh = FBXSerializer::extractMesh(objectChild, meshIndex, false);
// Callback to get MaterialID
GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) {
return extractedMesh.partMaterialTextures[partIndex].first;
};
// Compress mesh information and store in dracoMeshNode
FBXNode dracoMeshNode;
bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback);
// if bake fails - return, if there were errors and continue, if there were warnings.
if (!success) {
if (hasErrors()) {
return;
} else if (hasWarnings()) {
continue;
}
} else {
objectChild.children.push_back(dracoMeshNode);
static const std::vector<QString> nodeNamesToDelete {
// Node data that is packed into the draco mesh
"Vertices",
"PolygonVertexIndex",
"LayerElementNormal",
"LayerElementColor",
"LayerElementUV",
"LayerElementMaterial",
"LayerElementTexture",
// Node data that we don't support
"Edges",
"LayerElementTangent",
"LayerElementBinormal",
"LayerElementSmoothing"
};
auto& children = objectChild.children;
auto it = children.begin();
while (it != children.end()) {
auto begin = nodeNamesToDelete.begin();
auto end = nodeNamesToDelete.end();
if (find(begin, end, it->name) != end) {
it = children.erase(it);
} else {
++it;
for (FBXNode& object : rootChild.children) {
if (object.name == "Geometry") {
if (object.properties.at(2) == "Mesh") {
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
} 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
static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation");
static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation");
static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling");
for (int i = 0; i < modelChild.children.size(); i++) {
const auto& prop = modelChild.children[i];
const auto& propertyName = prop.properties.at(0);
if (propertyName == GEOMETRIC_TRANSLATION ||
propertyName == GEOMETRIC_ROTATION ||
propertyName == GEOMETRIC_SCALING) {
modelChild.children.removeAt(i);
--i;
}
}
} else if (modelChild.name == "Vertices") {
// This model is also a mesh
int meshNum = meshIndexToRuntimeOrder[meshIndex];
replaceMeshNodeWithDraco(object, dracoMeshes[meshNum], dracoMaterialLists[meshNum]);
meshIndex++;
}
}
} // Geometry Object
}
} // foreach root child
if (hasErrors()) {
return;
}
}
}
}
}

View file

@ -31,31 +31,18 @@ using TextureBakerThreadGetter = std::function<QThread*()>;
class FBXBaker : public ModelBaker {
Q_OBJECT
public:
using ModelBaker::ModelBaker;
FBXBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
public slots:
virtual void bake() override;
signals:
void sourceCopyReadyToLoad();
private slots:
void bakeSourceCopy();
void handleFBXNetworkReply();
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 setupOutputFolder();
void loadSourceFBX();
void importScene();
void embedTextureMetaData();
void rewriteAndBakeSceneModels();
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);
HFMModel* _hfmModel;
QHash<QString, int> _textureNameMatchCount;
QHash<QUrl, QString> _remappedTexturePaths;
hfm::Model::Pointer _hfmModel;
bool _pendingErrorEmission { false };
};

View file

@ -11,9 +11,11 @@
#include "JSBaker.h"
#include <PathUtils.h>
#include <QtNetwork/QNetworkReply>
#include "Baker.h"
#include <NetworkAccessManager.h>
#include <SharedUtil.h>
#include <PathUtils.h>
const int ASCII_CHARACTERS_UPPER_LIMIT = 126;
@ -21,25 +23,79 @@ JSBaker::JSBaker(const QUrl& jsURL, const QString& bakedOutputDir) :
_jsURL(jsURL),
_bakedOutputDir(bakedOutputDir)
{
}
void JSBaker::bake() {
qCDebug(js_baking) << "JS Baker " << _jsURL << "bake starting";
// Import file to start baking
QFile jsFile(_jsURL.toLocalFile());
if (!jsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
handleError("Error opening " + _jsURL.fileName() + " for reading");
return;
}
// once our script is loaded, kick off a the processing
connect(this, &JSBaker::originalScriptLoaded, this, &JSBaker::processScript);
if (_originalScript.isEmpty()) {
// first load the script (either locally or remotely)
loadScript();
} else {
// we already have a script passed to us, use that
processScript();
}
}
void JSBaker::loadScript() {
// check if the script is local or first needs to be downloaded
if (_jsURL.isLocalFile()) {
// load up the local file
QFile localScript(_jsURL.toLocalFile());
if (!localScript.open(QIODevice::ReadOnly | QIODevice::Text)) {
handleError("Error opening " + _jsURL.fileName() + " for reading");
return;
}
_originalScript = localScript.readAll();
emit originalScriptLoaded();
} else {
// remote file, kick off a download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_jsURL);
qCDebug(js_baking) << "Downloading" << _jsURL;
// kickoff the download, wait for slot to tell us it is done
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &JSBaker::handleScriptNetworkReply);
}
}
void JSBaker::handleScriptNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(js_baking) << "Downloaded script" << _jsURL;
// store the original script so it can be passed along for the bake
_originalScript = requestReply->readAll();
emit originalScriptLoaded();
} else {
// add an error to our list stating that this script could not be downloaded
handleError("Error downloading " + _jsURL.toString() + " - " + requestReply->errorString());
}
}
void JSBaker::processScript() {
// Read file into an array
QByteArray inputJS = jsFile.readAll();
QByteArray outputJS;
// Call baking on inputJS and store result in outputJS
bool success = bakeJS(inputJS, outputJS);
bool success = bakeJS(_originalScript, outputJS);
if (!success) {
qCDebug(js_baking) << "Bake Failed";
handleError("Unterminated multi-line comment");

View file

@ -25,11 +25,24 @@ public:
JSBaker(const QUrl& jsURL, const QString& bakedOutputDir);
static bool bakeJS(const QByteArray& inputFile, QByteArray& outputFile);
QString getJSPath() const { return _jsURL.toDisplayString(); }
QString getBakedJSFilePath() const { return _bakedJSFilePath; }
public slots:
virtual void bake() override;
signals:
void originalScriptLoaded();
private slots:
void processScript();
private:
void loadScript();
void handleScriptNetworkReply();
QUrl _jsURL;
QByteArray _originalScript;
QString _bakedOutputDir;
QString _bakedJSFilePath;

View file

@ -0,0 +1,247 @@
//
// MaterialBaker.cpp
// libraries/baking/src
//
// Created by Sam Gondelman on 2/26/2019
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "MaterialBaker.h"
#include <unordered_map>
#include "QJsonObject"
#include "QJsonDocument"
#include "MaterialBakingLoggingCategory.h"
#include <SharedUtil.h>
#include <PathUtils.h>
#include <graphics-scripting/GraphicsScriptingInterface.h>
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) :
_materialData(materialData),
_isURL(isURL),
_bakedOutputDir(bakedOutputDir),
_textureOutputDir(bakedOutputDir + "/materialTextures/" + QString::number(materialNum++)),
_destinationPath(destinationPath)
{
}
void MaterialBaker::bake() {
qDebug(material_baking) << "Material Baker" << _materialData << "bake starting";
// once our script is loaded, kick off a the processing
connect(this, &MaterialBaker::originalMaterialLoaded, this, &MaterialBaker::processMaterial);
if (!_materialResource) {
// first load the material (either locally or remotely)
loadMaterial();
} else {
// we already have a material passed to us, use that
if (_materialResource->isLoaded()) {
processMaterial();
} else {
connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded);
}
}
}
void MaterialBaker::loadMaterial() {
if (!_isURL) {
qCDebug(material_baking) << "Loading local material" << _materialData;
_materialResource = NetworkMaterialResourcePointer(new NetworkMaterialResource());
// TODO: add baseURL to allow these to reference relative files next to them
_materialResource->parsedMaterials = NetworkMaterialResource::parseJSONMaterials(QJsonDocument::fromJson(_materialData.toUtf8()), QUrl());
} else {
qCDebug(material_baking) << "Downloading material" << _materialData;
_materialResource = MaterialCache::instance().getMaterial(_materialData);
}
if (_materialResource) {
if (_materialResource->isLoaded()) {
emit originalMaterialLoaded();
} else {
connect(_materialResource.data(), &Resource::finished, this, &MaterialBaker::originalMaterialLoaded);
}
} else {
handleError("Error loading " + _materialData);
}
}
void MaterialBaker::processMaterial() {
if (!_materialResource || _materialResource->parsedMaterials.networkMaterials.size() == 0) {
handleError("Error processing " + _materialData);
return;
}
if (QDir(_textureOutputDir).exists()) {
qWarning() << "Output path" << _textureOutputDir << "already exists. Continuing.";
} else {
qCDebug(material_baking) << "Creating materialTextures output folder" << _textureOutputDir;
if (!QDir().mkpath(_textureOutputDir)) {
handleError("Failed to create materialTextures output folder " + _textureOutputDir);
}
}
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();
QUrl url = texture->getUrl();
QString cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString();
auto idx = cleanURL.lastIndexOf('.');
auto extension = idx >= 0 ? url.toDisplayString().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);
if (!_textureBakers.contains(textureKey)) {
auto baseTextureFileName = _textureFileNamer.createBaseTextureFileName(textureURL.fileName(), it->second);
QSharedPointer<TextureBaker> textureBaker {
new TextureBaker(textureURL, it->second, _textureOutputDir, "", baseTextureFileName),
&TextureBaker::deleteLater
};
textureBaker->setMapChannel(mapChannel);
connect(textureBaker.data(), &TextureBaker::finished, this, &MaterialBaker::handleFinishedTextureBaker);
_textureBakers.insert(textureKey, textureBaker);
textureBaker->moveToThread(_getNextOvenWorkerThreadOperator ? _getNextOvenWorkerThreadOperator() : thread());
QMetaObject::invokeMethod(textureBaker.data(), "bake");
}
_materialsNeedingRewrite.insert(textureKey, networkMaterial.second);
} else {
qCDebug(material_baking) << "Texture extension not supported: " << extension;
}
}
}
}
}
if (_textureBakers.empty()) {
outputMaterial();
}
}
void MaterialBaker::handleFinishedTextureBaker() {
auto baker = qobject_cast<TextureBaker*>(sender());
if (baker) {
QPair<QUrl, image::TextureUsage::Type> textureKey = { baker->getTextureURL(), baker->getTextureType() };
if (!baker->hasErrors()) {
// this TextureBaker is done and everything went according to plan
qCDebug(material_baking) << "Re-writing texture references to" << baker->getTextureURL();
auto newURL = QUrl(_textureOutputDir).resolved(baker->getMetaTextureFileName());
auto relativeURL = QDir(_bakedOutputDir).relativeFilePath(newURL.toString());
// Replace the old texture URLs
for (auto networkMaterial : _materialsNeedingRewrite.values(textureKey)) {
networkMaterial->getTextureMap(baker->getMapChannel())->getTextureSource()->setUrl(_destinationPath.resolved(relativeURL));
}
} else {
// this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from
// the texture to our warnings
_warningList << baker->getWarnings();
}
_materialsNeedingRewrite.remove(textureKey);
_textureBakers.remove(textureKey);
if (_textureBakers.empty()) {
outputMaterial();
}
} else {
handleWarning("Unidentified baker finished and signaled to material baker to handle texture. Material: " + _materialData);
}
}
void MaterialBaker::outputMaterial() {
if (_materialResource) {
QJsonObject json;
if (_materialResource->parsedMaterials.networkMaterials.size() == 1) {
auto networkMaterial = _materialResource->parsedMaterials.networkMaterials.begin();
auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial->second);
QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant();
json.insert("materials", QJsonDocument::fromVariant(materialVariant).object());
} else {
QJsonArray materialArray;
for (auto networkMaterial : _materialResource->parsedMaterials.networkMaterials) {
auto scriptableMaterial = scriptable::ScriptableMaterial(networkMaterial.second);
QVariant materialVariant = scriptable::scriptableMaterialToScriptValue(&_scriptEngine, scriptableMaterial).toVariant();
materialArray.append(QJsonDocument::fromVariant(materialVariant).object());
}
json.insert("materials", materialArray);
}
QByteArray outputMaterial = QJsonDocument(json).toJson(QJsonDocument::Compact);
if (_isURL) {
auto fileName = QUrl(_materialData).fileName();
auto baseName = fileName.left(fileName.lastIndexOf('.'));
auto bakedFilename = baseName + BAKED_MATERIAL_EXTENSION;
_bakedMaterialData = _bakedOutputDir + "/" + bakedFilename;
QFile bakedFile;
bakedFile.setFileName(_bakedMaterialData);
if (!bakedFile.open(QIODevice::WriteOnly)) {
handleError("Error opening " + _bakedMaterialData + " for writing");
return;
}
bakedFile.write(outputMaterial);
// Export successful
_outputFiles.push_back(_bakedMaterialData);
qCDebug(material_baking) << "Exported" << _materialData << "to" << _bakedMaterialData;
} else {
_bakedMaterialData = QString(outputMaterial);
qCDebug(material_baking) << "Converted" << _materialData << "to" << _bakedMaterialData;
}
}
// emit signal to indicate the material baking is finished
emit finished();
}

View file

@ -0,0 +1,67 @@
//
// MaterialBaker.h
// libraries/baking/src
//
// Created by Sam Gondelman on 2/26/2019
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_MaterialBaker_h
#define hifi_MaterialBaker_h
#include "Baker.h"
#include "TextureBaker.h"
#include "baking/TextureFileNamer.h"
#include <material-networking/MaterialCache.h>
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);
QString getMaterialData() const { return _materialData; }
bool isURL() const { return _isURL; }
QString getBakedMaterialData() const { return _bakedMaterialData; }
static void setNextOvenWorkerThreadOperator(std::function<QThread*()> getNextOvenWorkerThreadOperator) { _getNextOvenWorkerThreadOperator = getNextOvenWorkerThreadOperator; }
public slots:
virtual void bake() override;
signals:
void originalMaterialLoaded();
private slots:
void processMaterial();
void outputMaterial();
void handleFinishedTextureBaker();
private:
void loadMaterial();
QString _materialData;
bool _isURL;
NetworkMaterialResourcePointer _materialResource;
QHash<QPair<QUrl, image::TextureUsage::Type>, QSharedPointer<TextureBaker>> _textureBakers;
QMultiHash<QPair<QUrl, image::TextureUsage::Type>, std::shared_ptr<NetworkMaterial>> _materialsNeedingRewrite;
QString _bakedOutputDir;
QString _textureOutputDir;
QString _bakedMaterialData;
QUrl _destinationPath;
QScriptEngine _scriptEngine;
static std::function<QThread*()> _getNextOvenWorkerThreadOperator;
TextureFileNamer _textureFileNamer;
};
#endif // !hifi_MaterialBaker_h

View file

@ -0,0 +1,14 @@
//
// MaterialBakingLoggingCategory.cpp
// libraries/baking/src
//
// Created by Sam Gondelman on 2/26/2019
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "MaterialBakingLoggingCategory.h"
Q_LOGGING_CATEGORY(material_baking, "hifi.material-baking");

View file

@ -0,0 +1,19 @@
//
// MaterialBakingLoggingCategory.h
// libraries/baking/src
//
// Created by Sam Gondelman on 2/26/2019
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_MaterialBakingLoggingCategory_h
#define hifi_MaterialBakingLoggingCategory_h
#include <QtCore/QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(material_baking)
#endif // hifi_MaterialBakingLoggingCategory_h

View file

@ -12,8 +12,17 @@
#include "ModelBaker.h"
#include <PathUtils.h>
#include <NetworkAccessManager.h>
#include <DependencyManager.h>
#include <hfm/ModelFormatRegistry.h>
#include <FBXSerializer.h>
#include <model-baker/Baker.h>
#include <model-baker/PrepareJointsTask.h>
#include <FBXWriter.h>
#include <FSTReader.h>
#ifdef _WIN32
#pragma warning( push )
@ -31,37 +40,275 @@
#pragma warning( pop )
#endif
#include "baking/BakerLibrary.h"
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory) :
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
_modelURL(inputModelURL),
_bakedOutputDir(bakedOutputDirectory),
_originalOutputDir(originalOutputDirectory),
_textureThreadGetter(inputTextureThreadGetter)
_textureThreadGetter(inputTextureThreadGetter),
_hasBeenBaked(hasBeenBaked)
{
auto tempDir = PathUtils::generateTemporaryDir();
auto bakedFilename = _modelURL.fileName();
if (!hasBeenBaked) {
bakedFilename = bakedFilename.left(bakedFilename.lastIndexOf('.'));
bakedFilename += BAKED_FBX_EXTENSION;
}
_bakedModelURL = _bakedOutputDir + "/" + bakedFilename;
}
if (tempDir.isEmpty()) {
handleError("Failed to create a temporary directory.");
void ModelBaker::setOutputURLSuffix(const QUrl& outputURLSuffix) {
_outputURLSuffix = outputURLSuffix;
}
void ModelBaker::setMappingURL(const QUrl& mappingURL) {
_mappingURL = mappingURL;
}
void ModelBaker::setMapping(const hifi::VariantHash& mapping) {
_mapping = mapping;
}
QUrl ModelBaker::getFullOutputMappingURL() const {
QUrl appendedURL = _outputMappingURL;
appendedURL.setFragment(_outputURLSuffix.fragment());
appendedURL.setQuery(_outputURLSuffix.query());
appendedURL.setUserInfo(_outputURLSuffix.userInfo());
return appendedURL;
}
void ModelBaker::bake() {
qDebug() << "ModelBaker" << _modelURL << "bake starting";
// Setup the output folders for the results of this bake
initializeOutputDirs();
if (shouldStop()) {
return;
}
_modelTempDir = tempDir;
_originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName());
qDebug() << "Made temporary dir " << _modelTempDir;
qDebug() << "Origin file path: " << _originalModelFilePath;
connect(this, &ModelBaker::modelLoaded, this, &ModelBaker::bakeSourceCopy);
// make a local copy of the model
saveSourceModel();
}
ModelBaker::~ModelBaker() {
if (_modelTempDir.exists()) {
if (!_modelTempDir.remove(_originalModelFilePath)) {
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath;
void ModelBaker::initializeOutputDirs() {
// Attempt to make the output folders
// Warn if there is an output directory using the same name, unless we know a parent FST baker created them already
if (QDir(_bakedOutputDir).exists()) {
if (_mappingURL.isEmpty()) {
qWarning() << "Output path" << _bakedOutputDir << "already exists. Continuing.";
}
if (!_modelTempDir.rmdir(".")) {
qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir;
} else {
qCDebug(model_baking) << "Creating baked output folder" << _bakedOutputDir;
if (!QDir().mkpath(_bakedOutputDir)) {
handleError("Failed to create baked output folder " + _bakedOutputDir);
return;
}
}
QDir originalOutputDir { _originalOutputDir };
if (originalOutputDir.exists()) {
if (_mappingURL.isEmpty()) {
qWarning() << "Output path" << _originalOutputDir << "already exists. Continuing.";
}
} else {
qCDebug(model_baking) << "Creating original output folder" << _originalOutputDir;
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create original output folder " + _originalOutputDir);
return;
}
}
if (originalOutputDir.isReadable()) {
// The output directory is available. Use that to write/read the original model file
_originalOutputModelPath = originalOutputDir.filePath(_modelURL.fileName());
} else {
handleError("Unable to write to original output folder " + _originalOutputDir);
}
}
void ModelBaker::saveSourceModel() {
// check if the FBX is local or first needs to be downloaded
if (_modelURL.isLocalFile()) {
// load up the local file
QFile localModelURL { _modelURL.toLocalFile() };
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalOutputModelPath;
if (!localModelURL.exists()) {
//QMessageBox::warning(this, "Could not find " + _modelURL.toString(), "");
handleError("Could not find " + _modelURL.toString());
return;
}
localModelURL.copy(_originalOutputModelPath);
// emit our signal to start the import of the model source copy
emit modelLoaded();
} else {
// remote file, kick off a download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_modelURL);
qCDebug(model_baking) << "Downloading" << _modelURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &ModelBaker::handleModelNetworkReply);
}
}
void ModelBaker::handleModelNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _modelURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(_originalOutputModelPath);
qDebug(model_baking) << "Writing copy of original model file to" << _originalOutputModelPath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// add an error to the error list for this model stating that a duplicate of the original model could not be made
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalOutputModelPath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
return;
}
// close that file now that we are done writing to it
copyOfOriginal.close();
// emit our signal to start the import of the model source copy
emit modelLoaded();
} else {
// add an error to our list stating that the model could not be downloaded
handleError("Failed to download " + _modelURL.toString());
}
}
void ModelBaker::bakeSourceCopy() {
QFile modelFile(_originalOutputModelPath);
if (!modelFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalOutputModelPath + " for reading");
return;
}
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
{
auto serializer = DependencyManager::get<ModelFormatRegistry>()->getSerializerForMediaType(modelData, _modelURL, "");
if (!serializer) {
handleError("Could not recognize file type of model file " + _originalOutputModelPath);
return;
}
hifi::VariantHash serializerMapping = _mapping;
serializerMapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library
serializerMapping["deduplicateIndices"] = true; // Draco compression also deduplicates, but we might as well shave it off to save on some earlier processing (currently FBXSerializer only)
hfm::Model::Pointer loadedModel = serializer->read(modelData, serializerMapping, _modelURL);
// Temporarily support copying the pre-parsed node from FBXSerializer, for better performance in FBXBaker
// TODO: Pure HFM baking
std::shared_ptr<FBXSerializer> fbxSerializer = std::dynamic_pointer_cast<FBXSerializer>(serializer);
if (fbxSerializer) {
qCDebug(model_baking) << "Parsing" << _modelURL;
_rootNode = fbxSerializer->_rootNode;
}
baker::Baker baker(loadedModel, serializerMapping, _mappingURL);
auto config = baker.getConfiguration();
// Enable compressed draco mesh generation
config->getJobConfig("BuildDracoMesh")->setEnabled(true);
// Do not permit potentially lossy modification of joint data meant for runtime
((PrepareJointsConfig*)config->getJobConfig("PrepareJoints"))->passthrough = true;
// The resources parsed from this job will not be used for now
// TODO: Proper full baking of all materials for a model
config->getJobConfig("ParseMaterialMapping")->setEnabled(false);
// Begin hfm baking
baker.run();
bakedModel = 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);
if (shouldStop()) {
return;
}
// Output FST file, copying over input mappings if available
QString outputFSTFilename = !_mappingURL.isEmpty() ? _mappingURL.fileName() : _modelURL.fileName();
auto extensionStart = outputFSTFilename.indexOf(".");
if (extensionStart != -1) {
outputFSTFilename.resize(extensionStart);
}
outputFSTFilename += ".baked.fst";
QString outputFSTURL = _bakedOutputDir + "/" + outputFSTFilename;
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 };
if (!fstOutputFile.open(QIODevice::WriteOnly)) {
handleError("Failed to open file '" + outputFSTURL + "' for writing");
return;
}
if (fstOutputFile.write(fstOut) == -1) {
handleError("Failed to write to file '" + outputFSTURL + "'");
return;
}
_outputFiles.push_back(outputFSTURL);
_outputMappingURL = outputFSTURL;
// check if we're already done with textures (in case we had none to re-write)
checkIfTexturesFinished();
}
void ModelBaker::abort() {
@ -74,176 +321,36 @@ void ModelBaker::abort() {
}
}
bool ModelBaker::compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) {
if (mesh.wasCompressed) {
handleError("Cannot re-bake a file that contains compressed mesh");
bool ModelBaker::buildDracoMeshNode(FBXNode& dracoMeshNode, const QByteArray& dracoMeshBytes, const std::vector<hifi::ByteArray>& dracoMaterialList) {
if (dracoMeshBytes.isEmpty()) {
handleError("Failed to finalize the baking of a draco Geometry node");
return false;
}
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles{ 0 };
for (auto& part : mesh.parts) {
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
handleWarning("Found a mesh part with invalid index data, skipping");
continue;
}
numTriangles += part.quadTrianglesIndices.size() / 3;
numTriangles += part.triangleIndices.size() / 3;
}
if (numTriangles == 0) {
return false;
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals{ mesh.normals.size() > 0 };
bool hasColors{ mesh.colors.size() > 0 };
bool hasTexCoords{ mesh.texCoords.size() > 0 };
bool hasTexCoords1{ mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true;
bool needsOriginalIndices{ hasDeformers };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
uint16_t materialID;
for (auto& part : mesh.parts) {
materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex;
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&mesh.normals[idx0], &mesh.normals[idx1],
&mesh.normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
handleWarning("Failed to finalize the baking of a draco Geometry node");
return false;
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(0, 5);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
FBXNode dracoNode;
dracoNode.name = "DracoMesh";
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int)buffer.size()));
dracoNode.properties.append(value);
dracoNode.properties.append(QVariant::fromValue(dracoMeshBytes));
// Additional draco mesh node information
{
FBXNode fbxVersionNode;
fbxVersionNode.name = "FBXDracoMeshVersion";
fbxVersionNode.properties.append(FBX_DRACO_MESH_VERSION);
dracoNode.children.append(fbxVersionNode);
FBXNode dracoVersionNode;
dracoVersionNode.name = "DracoMeshVersion";
dracoVersionNode.properties.append(DRACO_MESH_VERSION);
dracoNode.children.append(dracoVersionNode);
FBXNode materialListNode;
materialListNode.name = "MaterialList";
for (const hifi::ByteArray& materialID : dracoMaterialList) {
materialListNode.properties.append(materialID);
}
dracoNode.children.append(materialListNode);
}
dracoMeshNode = dracoNode;
// Mesh compression successful return true
return true;
}
@ -274,45 +381,42 @@ QString ModelBaker::compressTexture(QString modelTextureFileName, image::Texture
if (!modelTextureFileInfo.filePath().isEmpty()) {
textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit());
}
auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull());
auto urlToTexture = getTextureURL(modelTextureFileInfo, !textureContent.isNull());
QString baseTextureFileName;
if (_remappedTexturePaths.contains(urlToTexture)) {
baseTextureFileName = _remappedTexturePaths[urlToTexture];
} else {
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
baseTextureFileName = createBaseTextureFileName(modelTextureFileInfo);
_remappedTexturePaths[urlToTexture] = baseTextureFileName;
}
QString baseTextureFileName = _textureFileNamer.createBaseTextureFileName(modelTextureFileInfo, textureType);
qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName
<< "to" << baseTextureFileName;
QString bakedTextureFilePath {
_bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX
};
QString bakedTextureFilePath {
_bakedOutputDir + "/" + baseTextureFileName + BAKED_META_TEXTURE_SUFFIX
};
textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX;
textureChild = baseTextureFileName + BAKED_META_TEXTURE_SUFFIX;
if (!_bakingTextures.contains(urlToTexture)) {
_outputFiles.push_back(bakedTextureFilePath);
// bake this texture asynchronously
bakeTexture(urlToTexture, textureType, _bakedOutputDir, baseTextureFileName, textureContent);
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 QUrl& textureURL, image::TextureUsage::Type textureType,
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
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(textureURL, textureType, outputDir, "../", bakedFilename, textureContent),
new TextureBaker(textureKey.first, textureKey.second, outputDir, "../", bakedFilename, textureContent),
&TextureBaker::deleteLater
};
@ -321,7 +425,7 @@ void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type t
connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture);
// keep a shared pointer to the baking texture
_bakingTextures.insert(textureURL, bakingTexture);
_bakingTextures.insert(textureKey, bakingTexture);
// start baking the texture on one of our available worker threads
bakingTexture->moveToThread(_textureThreadGetter());
@ -373,7 +477,7 @@ void ModelBaker::handleBakedTexture() {
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
_bakingTextures.remove(bakedTexture->getTextureURL());
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
checkIfTexturesFinished();
} else {
@ -384,7 +488,7 @@ void ModelBaker::handleBakedTexture() {
_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());
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
// abort any other ongoing texture bakes since we know we'll end up failing
for (auto& bakingTexture : _bakingTextures) {
@ -397,7 +501,7 @@ void ModelBaker::handleBakedTexture() {
// 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());
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
checkIfTexturesFinished();
}
@ -411,7 +515,7 @@ void ModelBaker::handleAbortedTexture() {
qDebug() << "Texture aborted: " << bakedTexture->getTextureURL();
if (bakedTexture) {
_bakingTextures.remove(bakedTexture->getTextureURL());
_bakingTextures.remove({ bakedTexture->getTextureURL(), bakedTexture->getTextureType() });
}
// since a texture we were baking aborted, our status is also aborted
@ -425,14 +529,11 @@ void ModelBaker::handleAbortedTexture() {
checkIfTexturesFinished();
}
QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, bool isEmbedded) {
QUrl urlToTexture;
// use QFileInfo to easily split up the existing texture filename into its components
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
if (isEmbedded) {
urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath();
urlToTexture = _modelURL.toString() + "/" + textureFileInfo.filePath();
} else {
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
// set the texture URL to the local texture that we have confirmed exists
@ -442,14 +543,14 @@ QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativ
// this is a relative file path which will require different handling
// depending on the location of the original model
if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
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(apparentRelativePath.absoluteFilePath());
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(apparentRelativePath.fileName());
urlToTexture = _modelURL.resolved(textureFileInfo.fileName());
}
}
}
@ -494,25 +595,6 @@ void ModelBaker::checkIfTexturesFinished() {
}
}
QString ModelBaker::createBaseTextureFileName(const QFileInfo& textureFileInfo) {
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
QString baseTextureFileName{ textureFileInfo.completeBaseName() };
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
baseTextureFileName += "-" + QString::number(nameMatches);
}
// increment the number of name matches
++nameMatches;
return baseTextureFileName;
}
void ModelBaker::setWasAborted(bool wasAborted) {
if (wasAborted != _wasAborted.load()) {
Baker::setWasAborted(wasAborted);
@ -588,31 +670,25 @@ void ModelBaker::embedTextureMetaData() {
}
void ModelBaker::exportScene() {
// save the relative path to this FBX inside our passed output folder
auto fileName = _modelURL.fileName();
auto baseName = fileName.left(fileName.lastIndexOf('.'));
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
_bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename;
auto fbxData = FBXWriter::encodeFBX(_rootNode);
QFile bakedFile(_bakedModelFilePath);
QString bakedModelURL = _bakedModelURL.toString();
QFile bakedFile(bakedModelURL);
if (!bakedFile.open(QIODevice::WriteOnly)) {
handleError("Error opening " + _bakedModelFilePath + " for writing");
handleError("Error opening " + bakedModelURL + " for writing");
return;
}
bakedFile.write(fbxData);
_outputFiles.push_back(_bakedModelFilePath);
_outputFiles.push_back(bakedModelURL);
#ifdef HIFI_DUMP_FBX
{
FBXToJSON fbxToJSON;
fbxToJSON << _rootNode;
QFileInfo modelFile(_bakedModelFilePath);
QFileInfo modelFile(_bakedModelURL.toString());
QString outFilename(modelFile.dir().absolutePath() + "/" + modelFile.completeBaseName() + "_FBX.json");
QFile jsonFile(outFilename);
if (jsonFile.open(QIODevice::WriteOnly)) {
@ -622,5 +698,5 @@ void ModelBaker::exportScene() {
}
#endif
qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath;
qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << bakedModelURL;
}

View file

@ -19,6 +19,7 @@
#include "Baker.h"
#include "TextureBaker.h"
#include "baking/TextureFileNamer.h"
#include "ModelBakingLoggingCategory.h"
@ -30,57 +31,84 @@
using TextureBakerThreadGetter = std::function<QThread*()>;
using GetMaterialIDCallback = std::function <int(int)>;
static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
static const QString FST_EXTENSION { ".fst" };
static const QString BAKED_FST_EXTENSION { ".baked.fst" };
static const QString FBX_EXTENSION { ".fbx" };
static const QString BAKED_FBX_EXTENSION { ".baked.fbx" };
static const QString OBJ_EXTENSION { ".obj" };
static const QString GLTF_EXTENSION { ".gltf" };
class ModelBaker : public Baker {
Q_OBJECT
public:
ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "");
virtual ~ModelBaker();
using TextureKey = QPair<QUrl, image::TextureUsage::Type>;
bool compressMesh(HFMMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr);
ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
void setOutputURLSuffix(const QUrl& urlSuffix);
void setMappingURL(const QUrl& mappingURL);
void setMapping(const hifi::VariantHash& mapping);
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; }
QString getBakedModelFilePath() const { return _bakedModelFilePath; }
virtual QUrl getFullOutputMappingURL() const;
QUrl getBakedModelURL() const { return _bakedModelURL; }
signals:
void modelLoaded();
public slots:
virtual void bake() override;
virtual void abort() override;
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;
QString _bakedModelFilePath;
QDir _modelTempDir;
QString _originalModelFilePath;
TextureBakerThreadGetter _textureThreadGetter;
QString _originalOutputModelPath;
QString _outputMappingURL;
QUrl _bakedModelURL;
protected slots:
void handleModelNetworkReply();
virtual void bakeSourceCopy();
private slots:
void handleBakedTexture();
void handleAbortedTexture();
private:
QString createBaseTextureFileName(const QFileInfo & textureFileInfo);
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir,
const QString & bakedFilename, const QByteArray & textureContent);
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);
TextureBakerThreadGetter _textureThreadGetter;
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
QMultiHash<TextureKey, QSharedPointer<TextureBaker>> _bakingTextures;
QHash<QString, int> _textureNameMatchCount;
QHash<QUrl, QString> _remappedTexturePaths;
bool _pendingErrorEmission{ false };
bool _pendingErrorEmission { false };
bool _hasBeenBaked { false };
TextureFileNamer _textureFileNamer;
};
#endif // hifi_ModelBaker_h

View file

@ -35,157 +35,51 @@ const QByteArray CONNECTIONS_NODE_PROPERTY = "OO";
const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP";
const QByteArray MESH = "Mesh";
void OBJBaker::bake() {
qDebug() << "OBJBaker" << _modelURL << "bake starting";
// trigger bakeOBJ once OBJ is loaded
connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ);
// make a local copy of the OBJ
loadOBJ();
}
void OBJBaker::loadOBJ() {
if (!QDir().mkpath(_bakedOutputDir)) {
handleError("Failed to create baked OBJ output folder " + _bakedOutputDir);
return;
}
if (!QDir().mkpath(_originalOutputDir)) {
handleError("Failed to create original OBJ output folder " + _originalOutputDir);
return;
}
// check if the OBJ is local or it needs to be downloaded
if (_modelURL.isLocalFile()) {
// loading the local OBJ
QFile localOBJ { _modelURL.toLocalFile() };
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
if (!localOBJ.exists()) {
handleError("Could not find " + _modelURL.toString());
return;
}
// make a copy in the output folder
if (!_originalOutputDir.isEmpty()) {
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
localOBJ.copy(_originalModelFilePath);
// local OBJ is loaded emit signal to trigger its baking
emit OBJLoaded();
} else {
// OBJ is remote, start download
auto& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
// setup the request to follow re-directs and always hit the network
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
networkRequest.setUrl(_modelURL);
qCDebug(model_baking) << "Downloading" << _modelURL;
auto networkReply = networkAccessManager.get(networkRequest);
connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply);
}
}
void OBJBaker::handleOBJNetworkReply() {
auto requestReply = qobject_cast<QNetworkReply*>(sender());
if (requestReply->error() == QNetworkReply::NoError) {
qCDebug(model_baking) << "Downloaded" << _modelURL;
// grab the contents of the reply and make a copy in the output folder
QFile copyOfOriginal(_originalModelFilePath);
qDebug(model_baking) << "Writing copy of original obj to" << _originalModelFilePath << copyOfOriginal.fileName();
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
// add an error to the error list for this obj stating that a duplicate of the original obj could not be made
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")");
return;
}
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
return;
}
// close that file now that we are done writing to it
copyOfOriginal.close();
if (!_originalOutputDir.isEmpty()) {
copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName());
}
// remote OBJ is loaded emit signal to trigger its baking
emit OBJLoaded();
} else {
// add an error to our list stating that the OBJ could not be downloaded
handleError("Failed to download " + _modelURL.toString());
}
}
void OBJBaker::bakeOBJ() {
// Read the OBJ file
QFile objFile(_originalModelFilePath);
if (!objFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalModelFilePath + " for reading");
return;
}
QByteArray objData = objFile.readAll();
OBJSerializer serializer;
QVariantHash mapping;
mapping["combineParts"] = true; // set true so that OBJSerializer reads material info from material library
auto geometry = serializer.read(objData, mapping, _modelURL);
void OBJBaker::bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) {
// Write OBJ Data as FBX tree nodes
createFBXNodeTree(_rootNode, *geometry);
checkIfTexturesFinished();
createFBXNodeTree(_rootNode, hfmModel, dracoMeshes[0]);
}
void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) {
void OBJBaker::createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh) {
// Make all generated nodes children of rootNode
rootNode.children = { FBXNode(), FBXNode(), FBXNode() };
FBXNode& globalSettingsNode = rootNode.children[0];
FBXNode& objectNode = rootNode.children[1];
FBXNode& connectionsNode = rootNode.children[2];
// Generating FBX Header Node
FBXNode headerNode;
headerNode.name = FBX_HEADER_EXTENSION;
// Generating global settings node
// Required for Unit Scale Factor
FBXNode globalSettingsNode;
globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME;
// Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties
FBXNode properties70Node;
properties70Node.name = PROPERTIES70_NODE_NAME;
FBXNode pNode;
{
pNode.name = P_NODE_NAME;
pNode.properties.append({
"UnitScaleFactor", "double", "Number", "",
UNIT_SCALE_FACTOR
});
globalSettingsNode.children.push_back(FBXNode());
FBXNode& properties70Node = globalSettingsNode.children.back();
properties70Node.name = PROPERTIES70_NODE_NAME;
FBXNode pNode;
{
pNode.name = P_NODE_NAME;
pNode.properties.append({
"UnitScaleFactor", "double", "Number", "",
UNIT_SCALE_FACTOR
});
}
properties70Node.children = { pNode };
}
properties70Node.children = { pNode };
globalSettingsNode.children = { properties70Node };
// Generating Object node
FBXNode objectNode;
objectNode.name = OBJECTS_NODE_NAME;
objectNode.children = { FBXNode(), FBXNode() };
FBXNode& geometryNode = objectNode.children[0];
FBXNode& modelNode = objectNode.children[1];
// Generating Object node's child - Geometry node
FBXNode geometryNode;
// Generating Object node's child - Geometry node
geometryNode.name = GEOMETRY_NODE_NAME;
NodeID geometryID;
{
@ -196,15 +90,8 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) {
MESH
};
}
// Compress the mesh information and store in dracoNode
bool hasDeformers = false; // No concept of deformers for an OBJ
FBXNode dracoNode;
compressMesh(hfmModel.meshes[0], hasDeformers, dracoNode);
geometryNode.children.append(dracoNode);
// Generating Object node's child - Model node
FBXNode modelNode;
modelNode.name = MODEL_NODE_NAME;
NodeID modelID;
{
@ -212,16 +99,14 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) {
modelNode.properties = { modelID, MODEL_NODE_NAME, MESH };
}
objectNode.children = { geometryNode, modelNode };
// Generating Objects node's child - Material node
auto& meshParts = hfmModel.meshes[0].parts;
auto& meshParts = hfmModel->meshes[0].parts;
for (auto& meshPart : meshParts) {
FBXNode materialNode;
materialNode.name = MATERIAL_NODE_NAME;
if (hfmModel.materials.size() == 1) {
if (hfmModel->materials.size() == 1) {
// case when no material information is provided, OBJSerializer considers it as a single default material
for (auto& materialID : hfmModel.materials.keys()) {
for (auto& materialID : hfmModel->materials.keys()) {
setMaterialNodeProperties(materialNode, materialID, hfmModel);
}
} else {
@ -231,12 +116,28 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) {
objectNode.children.append(materialNode);
}
// Store the draco node containing the compressed mesh information, along with the per-meshPart material IDs the draco node references
// Because we redefine the material IDs when initializing the material nodes above, we pass that in for the material list
// The nth mesh part gets the nth material
if (!dracoMesh.isEmpty()) {
std::vector<hifi::ByteArray> newMaterialList;
newMaterialList.reserve(_materialIDs.size());
for (auto materialID : _materialIDs) {
newMaterialList.push_back(hifi::ByteArray(std::to_string((int)materialID).c_str()));
}
FBXNode dracoNode;
buildDracoMeshNode(dracoNode, dracoMesh, newMaterialList);
geometryNode.children.append(dracoNode);
} else {
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];
HFMMaterial currentMaterial = hfmModel->materials[material];
if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) {
auto textureID = nextNodeID();
_mapTextureMaterial.emplace_back(textureID, i);
@ -281,14 +182,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) {
}
// Generating Connections node
FBXNode connectionsNode;
connectionsNode.name = CONNECTIONS_NODE_NAME;
// connect Geometry to Model
FBXNode cNode;
cNode.name = C_NODE_NAME;
cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID };
connectionsNode.children = { cNode };
// connect Geometry to Model
{
FBXNode cNode;
cNode.name = C_NODE_NAME;
cNode.properties = { CONNECTIONS_NODE_PROPERTY, geometryID, modelID };
connectionsNode.children.push_back(cNode);
}
// connect all materials to model
for (auto& materialID : _materialIDs) {
@ -320,18 +222,15 @@ void OBJBaker::createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel) {
};
connectionsNode.children.append(cDiffuseNode);
}
// Make all generated nodes children of rootNode
rootNode.children = { globalSettingsNode, objectNode, connectionsNode };
}
// Set properties for material nodes
void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel) {
void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel) {
auto materialID = nextNodeID();
_materialIDs.push_back(materialID);
materialNode.properties = { materialID, material, MESH };
HFMMaterial currentMaterial = hfmModel.materials[material];
HFMMaterial currentMaterial = hfmModel->materials[material];
// Setting the hierarchy: Material -> Properties70 -> P -> Properties
FBXNode properties70Node;

View file

@ -27,20 +27,12 @@ class OBJBaker : public ModelBaker {
public:
using ModelBaker::ModelBaker;
public slots:
virtual void bake() override;
signals:
void OBJLoaded();
private slots:
void bakeOBJ();
void handleOBJNetworkReply();
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 loadOBJ();
void createFBXNodeTree(FBXNode& rootNode, HFMModel& hfmModel);
void setMaterialNodeProperties(FBXNode& materialNode, QString material, HFMModel& hfmModel);
void createFBXNodeTree(FBXNode& rootNode, const hfm::Model::Pointer& hfmModel, const hifi::ByteArray& dracoMesh);
void setMaterialNodeProperties(FBXNode& materialNode, QString material, const hfm::Model::Pointer& hfmModel);
NodeID nextNodeID() { return _nodeID++; }

View file

@ -47,6 +47,14 @@ TextureBaker::TextureBaker(const QUrl& textureURL, image::TextureUsage::Type tex
auto originalFilename = textureURL.fileName();
_baseFilename = originalFilename.left(originalFilename.lastIndexOf('.'));
}
auto textureFilename = _textureURL.fileName();
QString originalExtension;
int extensionStart = textureFilename.indexOf(".");
if (extensionStart != -1) {
originalExtension = textureFilename.mid(extensionStart);
}
_originalCopyFilePath = _outputDirectory.absoluteFilePath(_baseFilename + originalExtension);
}
void TextureBaker::bake() {
@ -128,7 +136,9 @@ void TextureBaker::processTexture() {
TextureMeta meta;
auto originalCopyFilePath = _outputDirectory.absoluteFilePath(_textureURL.fileName());
QString originalCopyFilePath = _originalCopyFilePath.toString();
// Copy the original file into the baked output directory if it doesn't exist yet
{
QFile file { originalCopyFilePath };
if (!file.open(QIODevice::WriteOnly) || file.write(_originalTexture) == -1) {
@ -138,9 +148,10 @@ void TextureBaker::processTexture() {
// IMPORTANT: _originalTexture is empty past this point
_originalTexture.clear();
_outputFiles.push_back(originalCopyFilePath);
meta.original = _metaTexturePathPrefix + _textureURL.fileName();
meta.original = _metaTexturePathPrefix + _originalCopyFilePath.fileName();
}
// Load the copy of the original file from the baked output directory. New images will be created using the original as the source data.
auto buffer = std::static_pointer_cast<QIODevice>(std::make_shared<QFile>(originalCopyFilePath));
if (!buffer->open(QIODevice::ReadOnly)) {
handleError("Could not open original file at " + originalCopyFilePath);

View file

@ -22,6 +22,8 @@
#include "Baker.h"
#include <material-networking/MaterialCache.h>
extern const QString BAKED_TEXTURE_KTX_EXT;
extern const QString BAKED_META_TEXTURE_SUFFIX;
@ -37,12 +39,18 @@ public:
QUrl getTextureURL() const { return _textureURL; }
QString getBaseFilename() const { return _baseFilename; }
QString getMetaTextureFileName() const { return _metaTextureFileName; }
virtual void setWasAborted(bool wasAborted) override;
static void setCompressionEnabled(bool enabled) { _compressionEnabled = enabled; }
void setMapChannel(graphics::Material::MapChannel mapChannel) { _mapChannel = mapChannel; }
graphics::Material::MapChannel getMapChannel() const { return _mapChannel; }
image::TextureUsage::Type getTextureType() const { return _textureType; }
public slots:
virtual void bake() override;
virtual void abort() override;
@ -60,11 +68,14 @@ private:
QUrl _textureURL;
QByteArray _originalTexture;
image::TextureUsage::Type _textureType;
graphics::Material::MapChannel _mapChannel;
bool _mapChannelSet { false };
QString _baseFilename;
QDir _outputDirectory;
QString _metaTextureFileName;
QString _metaTexturePathPrefix;
QUrl _originalCopyFilePath;
std::atomic<bool> _abortProcessing { false };

View file

@ -0,0 +1,83 @@
//
// BakerLibrary.cpp
// libraries/baking/src/baking
//
// Created by Sabrina Shanman on 2019/02/14.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "BakerLibrary.h"
#include "FSTBaker.h"
#include "../FBXBaker.h"
#include "../OBJBaker.h"
// Check if the file pointed to by this URL is a bakeable model, by comparing extensions
QUrl getBakeableModelURL(const QUrl& url) {
static const std::vector<QString> extensionsToBake = {
FST_EXTENSION,
BAKED_FST_EXTENSION,
FBX_EXTENSION,
BAKED_FBX_EXTENSION,
OBJ_EXTENSION,
GLTF_EXTENSION
};
QUrl cleanURL = url.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
QString cleanURLString = cleanURL.fileName();
for (auto& extension : extensionsToBake) {
if (cleanURLString.endsWith(extension, Qt::CaseInsensitive)) {
return cleanURL;
}
}
qWarning() << "Unknown model type: " << url.fileName();
return QUrl();
}
bool isModelBaked(const QUrl& bakeableModelURL) {
auto modelString = bakeableModelURL.toString();
auto beforeModelExtension = modelString;
beforeModelExtension.resize(modelString.lastIndexOf('.'));
return beforeModelExtension.endsWith(".baked");
}
std::unique_ptr<ModelBaker> getModelBaker(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, 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
auto baseName = filename.left(filename.lastIndexOf('.')).left(filename.lastIndexOf(".baked"));
auto subDirName = "/" + baseName;
int i = 1;
while (QDir(contentOutputPath + subDirName).exists()) {
subDirName = "/" + baseName + "-" + QString::number(i++);
}
QString bakedOutputDirectory = contentOutputPath + subDirName + "/baked";
QString originalOutputDirectory = contentOutputPath + subDirName + "/original";
return getModelBakerWithOutputDirectories(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
}
std::unique_ptr<ModelBaker> getModelBakerWithOutputDirectories(const QUrl& bakeableModelURL, TextureBakerThreadGetter inputTextureThreadGetter, 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));
} else if (filename.endsWith(FBX_EXTENSION, Qt::CaseInsensitive)) {
baker = std::make_unique<FBXBaker>(bakeableModelURL, inputTextureThreadGetter, 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);
//} else if (filename.endsWith(GLTF_EXTENSION, Qt::CaseInsensitive)) {
//baker = std::make_unique<GLTFBaker>(bakeableModelURL, inputTextureThreadGetter, bakedOutputDirectory, originalOutputDirectory);
} else {
qDebug() << "Could not create ModelBaker for url" << bakeableModelURL;
}
return baker;
}

View file

@ -0,0 +1,31 @@
//
// ModelBaker.h
// libraries/baking/src/baking
//
// Created by Sabrina Shanman on 2019/02/14.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_BakerLibrary_h
#define hifi_BakerLibrary_h
#include <QUrl>
#include "../ModelBaker.h"
// Returns either the given model URL if valid, or an empty URL
QUrl getBakeableModelURL(const QUrl& url);
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);
// 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);
#endif // hifi_BakerLibrary_h

View file

@ -0,0 +1,128 @@
//
// FSTBaker.cpp
// libraries/baking/src/baking
//
// Created by Sabrina Shanman on 2019/03/06.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "FSTBaker.h"
#include <PathUtils.h>
#include <NetworkAccessManager.h>
#include "BakerLibrary.h"
#include <FSTReader.h>
FSTBaker::FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory, bool hasBeenBaked) :
ModelBaker(inputMappingURL, inputTextureThreadGetter, 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));
QUrl newInputMappingURL = inputMappingURL.adjusted(QUrl::RemoveFilename).resolved(originalRelativePath);
_modelURL = newInputMappingURL;
}
_mappingURL = _modelURL;
{
// Unused, but defined for consistency
auto bakedFilename = _modelURL.fileName();
bakedFilename.replace(FST_EXTENSION, BAKED_FST_EXTENSION);
_bakedModelURL = _bakedOutputDir + "/" + bakedFilename;
}
}
QUrl FSTBaker::getFullOutputMappingURL() const {
if (_modelBaker) {
return _modelBaker->getFullOutputMappingURL();
}
return QUrl();
}
void FSTBaker::bakeSourceCopy() {
if (shouldStop()) {
return;
}
QFile fstFile(_originalOutputModelPath);
if (!fstFile.open(QIODevice::ReadOnly)) {
handleError("Error opening " + _originalOutputModelPath + " for reading");
return;
}
hifi::ByteArray fstData = fstFile.readAll();
_mapping = FSTReader::readMapping(fstData);
auto filenameField = _mapping[FILENAME_FIELD].toString();
if (filenameField.isEmpty()) {
handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be found");
return;
}
auto modelURL = _mappingURL.adjusted(QUrl::RemoveFilename).resolved(filenameField);
auto bakeableModelURL = getBakeableModelURL(modelURL);
if (bakeableModelURL.isEmpty()) {
handleError("The '" + FILENAME_FIELD + "' property in the FST file '" + _originalOutputModelPath + "' could not be resolved to a valid bakeable model url");
return;
}
auto baker = getModelBakerWithOutputDirectories(bakeableModelURL, _textureThreadGetter, _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");
return;
}
if (dynamic_cast<FSTBaker*>(_modelBaker.get())) {
// Could be interesting, but for now let's just prevent infinite FST loops in the most straightforward way possible
handleError("The FST file '" + _originalOutputModelPath + "' (property: '" + FILENAME_FIELD + "') references another FST file. FST chaining is not supported.");
return;
}
_modelBaker->setMappingURL(_mappingURL);
_modelBaker->setMapping(_mapping);
// Hold on to the old url userinfo/query/fragment data so ModelBaker::getFullOutputMappingURL retains that data from the original model URL
_modelBaker->setOutputURLSuffix(modelURL);
connect(_modelBaker.get(), &ModelBaker::aborted, this, &FSTBaker::handleModelBakerAborted);
connect(_modelBaker.get(), &ModelBaker::finished, this, &FSTBaker::handleModelBakerFinished);
// FSTBaker can't do much while waiting for the ModelBaker to finish, so start the bake on this thread.
_modelBaker->bake();
}
void FSTBaker::handleModelBakerEnded() {
for (auto& warning : _modelBaker->getWarnings()) {
_warningList.push_back(warning);
}
for (auto& error : _modelBaker->getErrors()) {
_errorList.push_back(error);
}
// Get the output files, including but not limited to the FST file and the baked model file
for (auto& outputFile : _modelBaker->getOutputFiles()) {
_outputFiles.push_back(outputFile);
}
}
void FSTBaker::handleModelBakerAborted() {
handleModelBakerEnded();
if (!wasAborted()) {
setWasAborted(true);
}
}
void FSTBaker::handleModelBakerFinished() {
handleModelBakerEnded();
setIsFinished(true);
}
void FSTBaker::abort() {
ModelBaker::abort();
if (_modelBaker) {
_modelBaker->abort();
}
}

View file

@ -0,0 +1,45 @@
//
// FSTBaker.h
// libraries/baking/src/baking
//
// Created by Sabrina Shanman on 2019/03/06.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_FSTBaker_h
#define hifi_FSTBaker_h
#include "../ModelBaker.h"
class FSTBaker : public ModelBaker {
Q_OBJECT
public:
FSTBaker(const QUrl& inputMappingURL, TextureBakerThreadGetter inputTextureThreadGetter,
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "", bool hasBeenBaked = false);
virtual QUrl getFullOutputMappingURL() const override;
signals:
void fstLoaded();
public slots:
virtual void abort() override;
protected:
std::unique_ptr<ModelBaker> _modelBaker;
protected slots:
virtual void bakeSourceCopy() override;
virtual void bakeProcessedSource(const hfm::Model::Pointer& hfmModel, const std::vector<hifi::ByteArray>& dracoMeshes, const std::vector<std::vector<hifi::ByteArray>>& dracoMaterialLists) override {};
void handleModelBakerAborted();
void handleModelBakerFinished();
private:
void handleModelBakerEnded();
};
#endif // hifi_FSTBaker_h

View file

@ -0,0 +1,34 @@
//
// TextureFileNamer.cpp
// libraries/baking/src/baking
//
// Created by Sabrina Shanman on 2019/03/14.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "TextureFileNamer.h"
QString TextureFileNamer::createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType) {
// If two textures have the same URL but are used differently, we need to process them separately
QString addMapChannel = QString::fromStdString("_" + std::to_string(textureType));
QString baseTextureFileName{ textureFileInfo.baseName() + addMapChannel };
// first make sure we have a unique base name for this texture
// in case another texture referenced by this model has the same base name
auto& nameMatches = _textureNameMatchCount[baseTextureFileName];
if (nameMatches > 0) {
// there are already nameMatches texture with this name
// append - and that number to our baked texture file name so that it is unique
baseTextureFileName += "-" + QString::number(nameMatches);
}
// increment the number of name matches
++nameMatches;
return baseTextureFileName;
}

View file

@ -0,0 +1,30 @@
//
// TextureFileNamer.h
// libraries/baking/src/baking
//
// Created by Sabrina Shanman on 2019/03/14.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_TextureFileNamer_h
#define hifi_TextureFileNamer_h
#include <QtCore/QFileInfo>
#include <QHash>
#include <image/Image.h>
class TextureFileNamer {
public:
TextureFileNamer() {}
QString createBaseTextureFileName(const QFileInfo& textureFileInfo, const image::TextureUsage::Type textureType);
protected:
QHash<QString, int> _textureNameMatchCount;
};
#endif // hifi_TextureFileNamer_h

View file

@ -13,27 +13,26 @@
#define hifi_FBX_h_
#include <QMetaType>
#include <QVarLengthArray>
#include <QVariant>
#include <QVector>
#include <glm/glm.hpp>
#include <shared/HifiTypes.h>
// See comment in FBXSerializer::parseFBX().
static const int FBX_HEADER_BYTES_BEFORE_VERSION = 23;
static const QByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary ");
static const QByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3);
static const hifi::ByteArray FBX_BINARY_PROLOG("Kaydara FBX Binary ");
static const hifi::ByteArray FBX_BINARY_PROLOG2("\0\x1a\0", 3);
static const quint32 FBX_VERSION_2015 = 7400;
static const quint32 FBX_VERSION_2016 = 7500;
static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000;
static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES;
static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1;
static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2;
static const int32_t FBX_PROPERTY_UNCOMPRESSED_FLAG = 0;
static const int32_t FBX_PROPERTY_COMPRESSED_FLAG = 1;
// The version of the FBX node containing the draco mesh. See also: DRACO_MESH_VERSION in HFM.h
static const int FBX_DRACO_MESH_VERSION = 2;
class FBXNode;
using FBXNodeList = QList<FBXNode>;
@ -41,7 +40,7 @@ using FBXNodeList = QList<FBXNode>;
/// A node within an FBX document.
class FBXNode {
public:
QByteArray name;
hifi::ByteArray name;
QVariantList properties;
FBXNodeList children;
};

View file

@ -178,7 +178,7 @@ public:
void printNode(const FBXNode& node, int indentLevel) {
int indentLength = 2;
QByteArray spaces(indentLevel * indentLength, ' ');
hifi::ByteArray spaces(indentLevel * indentLength, ' ');
QDebug nodeDebug = qDebug(modelformat);
nodeDebug.nospace() << spaces.data() << node.name.data() << ": ";
@ -308,7 +308,7 @@ public:
};
bool checkMaterialsHaveTextures(const QHash<QString, HFMMaterial>& materials,
const QHash<QString, QByteArray>& textureFilenames, const QMultiMap<QString, QString>& _connectionChildMap) {
const QHash<QString, hifi::ByteArray>& textureFilenames, const QMultiMap<QString, QString>& _connectionChildMap) {
foreach (const QString& materialID, materials.keys()) {
foreach (const QString& childID, _connectionChildMap.values(materialID)) {
if (textureFilenames.contains(childID)) {
@ -375,7 +375,7 @@ HFMLight extractLight(const FBXNode& object) {
return light;
}
QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) {
// in order to match the behaviour when loading models from remote URLs
// we assume that all external textures are right beside the loaded model
// ignoring any relative paths or absolute paths inside of models
@ -383,8 +383,10 @@ QByteArray fileOnUrl(const QByteArray& filepath, const QString& url) {
return filepath.mid(filepath.lastIndexOf('/') + 1);
}
HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QString& url) {
HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) {
const FBXNode& node = _rootNode;
bool deduplicateIndices = mapping["deduplicateIndices"].toBool();
QMap<QString, ExtractedMesh> meshes;
QHash<QString, QString> modelIDsToNames;
QHash<QString, int> meshIDsToMeshIndices;
@ -406,11 +408,11 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
std::map<QString, HFMLight> lights;
QVariantHash blendshapeMappings = mapping.value("bs").toHash();
hifi::VariantHash blendshapeMappings = mapping.value("bs").toHash();
QMultiHash<QByteArray, WeightedIndex> blendshapeIndices;
QMultiHash<hifi::ByteArray, WeightedIndex> blendshapeIndices;
for (int i = 0;; i++) {
QByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i];
hifi::ByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i];
if (blendshapeName.isEmpty()) {
break;
}
@ -455,7 +457,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
}
} else if (subobject.name == "Properties70") {
foreach (const FBXNode& subsubobject, subobject.children) {
static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName"));
static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName"));
if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 &&
subsubobject.properties.at(0) == APPLICATION_NAME) {
hfmModel.applicationName = subsubobject.properties.at(4).toString();
@ -472,9 +474,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
int index = 4;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == propertyName) {
static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor");
static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor");
static const QVariant UP_AXIS = QByteArray("UpAxis");
static const QVariant UNIT_SCALE_FACTOR = hifi::ByteArray("UnitScaleFactor");
static const QVariant AMBIENT_COLOR = hifi::ByteArray("AmbientColor");
static const QVariant UP_AXIS = hifi::ByteArray("UpAxis");
const auto& subpropName = subobject.properties.at(0);
if (subpropName == UNIT_SCALE_FACTOR) {
unitScaleFactor = subobject.properties.at(index).toFloat();
@ -499,7 +501,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
foreach (const FBXNode& object, child.children) {
if (object.name == "Geometry") {
if (object.properties.at(2) == "Mesh") {
meshes.insert(getID(object.properties), extractMesh(object, meshIndex));
meshes.insert(getID(object.properties), extractMesh(object, meshIndex, deduplicateIndices));
} else { // object.properties.at(2) == "Shape"
ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) };
blendshapes.append(extracted);
@ -540,7 +542,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
QVector<ExtractedBlendshape> blendshapes;
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
QByteArray propertyName;
hifi::ByteArray propertyName;
int index;
if (subobject.name == "Properties60") {
properties = true;
@ -553,27 +555,27 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
index = 4;
}
if (properties) {
static const QVariant ROTATION_ORDER = QByteArray("RotationOrder");
static const QVariant GEOMETRIC_TRANSLATION = QByteArray("GeometricTranslation");
static const QVariant GEOMETRIC_ROTATION = QByteArray("GeometricRotation");
static const QVariant GEOMETRIC_SCALING = QByteArray("GeometricScaling");
static const QVariant LCL_TRANSLATION = QByteArray("Lcl Translation");
static const QVariant LCL_ROTATION = QByteArray("Lcl Rotation");
static const QVariant LCL_SCALING = QByteArray("Lcl Scaling");
static const QVariant ROTATION_MAX = QByteArray("RotationMax");
static const QVariant ROTATION_MAX_X = QByteArray("RotationMaxX");
static const QVariant ROTATION_MAX_Y = QByteArray("RotationMaxY");
static const QVariant ROTATION_MAX_Z = QByteArray("RotationMaxZ");
static const QVariant ROTATION_MIN = QByteArray("RotationMin");
static const QVariant ROTATION_MIN_X = QByteArray("RotationMinX");
static const QVariant ROTATION_MIN_Y = QByteArray("RotationMinY");
static const QVariant ROTATION_MIN_Z = QByteArray("RotationMinZ");
static const QVariant ROTATION_OFFSET = QByteArray("RotationOffset");
static const QVariant ROTATION_PIVOT = QByteArray("RotationPivot");
static const QVariant SCALING_OFFSET = QByteArray("ScalingOffset");
static const QVariant SCALING_PIVOT = QByteArray("ScalingPivot");
static const QVariant PRE_ROTATION = QByteArray("PreRotation");
static const QVariant POST_ROTATION = QByteArray("PostRotation");
static const QVariant ROTATION_ORDER = hifi::ByteArray("RotationOrder");
static const QVariant GEOMETRIC_TRANSLATION = hifi::ByteArray("GeometricTranslation");
static const QVariant GEOMETRIC_ROTATION = hifi::ByteArray("GeometricRotation");
static const QVariant GEOMETRIC_SCALING = hifi::ByteArray("GeometricScaling");
static const QVariant LCL_TRANSLATION = hifi::ByteArray("Lcl Translation");
static const QVariant LCL_ROTATION = hifi::ByteArray("Lcl Rotation");
static const QVariant LCL_SCALING = hifi::ByteArray("Lcl Scaling");
static const QVariant ROTATION_MAX = hifi::ByteArray("RotationMax");
static const QVariant ROTATION_MAX_X = hifi::ByteArray("RotationMaxX");
static const QVariant ROTATION_MAX_Y = hifi::ByteArray("RotationMaxY");
static const QVariant ROTATION_MAX_Z = hifi::ByteArray("RotationMaxZ");
static const QVariant ROTATION_MIN = hifi::ByteArray("RotationMin");
static const QVariant ROTATION_MIN_X = hifi::ByteArray("RotationMinX");
static const QVariant ROTATION_MIN_Y = hifi::ByteArray("RotationMinY");
static const QVariant ROTATION_MIN_Z = hifi::ByteArray("RotationMinZ");
static const QVariant ROTATION_OFFSET = hifi::ByteArray("RotationOffset");
static const QVariant ROTATION_PIVOT = hifi::ByteArray("RotationPivot");
static const QVariant SCALING_OFFSET = hifi::ByteArray("ScalingOffset");
static const QVariant SCALING_PIVOT = hifi::ByteArray("ScalingPivot");
static const QVariant PRE_ROTATION = hifi::ByteArray("PreRotation");
static const QVariant POST_ROTATION = hifi::ByteArray("PostRotation");
foreach(const FBXNode& property, subobject.children) {
const auto& childProperty = property.properties.at(0);
if (property.name == propertyName) {
@ -643,10 +645,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
}
}
}
} else if (subobject.name == "Vertices") {
} else if (subobject.name == "Vertices" || subobject.name == "DracoMesh") {
// it's a mesh as well as a model
mesh = &meshes[getID(object.properties)];
*mesh = extractMesh(object, meshIndex);
*mesh = extractMesh(object, meshIndex, deduplicateIndices);
} else if (subobject.name == "Shape") {
ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(),
@ -713,8 +715,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
const int MODEL_UV_SCALING_MIN_SIZE = 2;
const int CROPPING_MIN_SIZE = 4;
if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) {
QByteArray filename = subobject.properties.at(0).toByteArray();
QByteArray filepath = filename.replace('\\', '/');
hifi::ByteArray filename = subobject.properties.at(0).toByteArray();
hifi::ByteArray filepath = filename.replace('\\', '/');
filename = fileOnUrl(filepath, url);
_textureFilepaths.insert(getID(object.properties), filepath);
_textureFilenames.insert(getID(object.properties), filename);
@ -743,17 +745,17 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
subobject.properties.at(2).value<int>(),
subobject.properties.at(3).value<int>()));
} else if (subobject.name == "Properties70") {
QByteArray propertyName;
hifi::ByteArray propertyName;
int index;
propertyName = "P";
index = 4;
foreach (const FBXNode& property, subobject.children) {
static const QVariant UV_SET = QByteArray("UVSet");
static const QVariant CURRENT_TEXTURE_BLEND_MODE = QByteArray("CurrentTextureBlendMode");
static const QVariant USE_MATERIAL = QByteArray("UseMaterial");
static const QVariant TRANSLATION = QByteArray("Translation");
static const QVariant ROTATION = QByteArray("Rotation");
static const QVariant SCALING = QByteArray("Scaling");
static const QVariant UV_SET = hifi::ByteArray("UVSet");
static const QVariant CURRENT_TEXTURE_BLEND_MODE = hifi::ByteArray("CurrentTextureBlendMode");
static const QVariant USE_MATERIAL = hifi::ByteArray("UseMaterial");
static const QVariant TRANSLATION = hifi::ByteArray("Translation");
static const QVariant ROTATION = hifi::ByteArray("Rotation");
static const QVariant SCALING = hifi::ByteArray("Scaling");
if (property.name == propertyName) {
QString v = property.properties.at(0).toString();
if (property.properties.at(0) == UV_SET) {
@ -807,8 +809,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
_textureParams.insert(getID(object.properties), tex);
}
} else if (object.name == "Video") {
QByteArray filepath;
QByteArray content;
hifi::ByteArray filepath;
hifi::ByteArray content;
foreach (const FBXNode& subobject, object.children) {
if (subobject.name == "RelativeFilename") {
filepath = subobject.properties.at(0).toByteArray();
@ -828,7 +830,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
foreach (const FBXNode& subobject, object.children) {
bool properties = false;
QByteArray propertyName;
hifi::ByteArray propertyName;
int index;
if (subobject.name == "Properties60") {
properties = true;
@ -845,31 +847,31 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
if (properties) {
std::vector<std::string> unknowns;
static const QVariant DIFFUSE_COLOR = QByteArray("DiffuseColor");
static const QVariant DIFFUSE_FACTOR = QByteArray("DiffuseFactor");
static const QVariant DIFFUSE = QByteArray("Diffuse");
static const QVariant SPECULAR_COLOR = QByteArray("SpecularColor");
static const QVariant SPECULAR_FACTOR = QByteArray("SpecularFactor");
static const QVariant SPECULAR = QByteArray("Specular");
static const QVariant EMISSIVE_COLOR = QByteArray("EmissiveColor");
static const QVariant EMISSIVE_FACTOR = QByteArray("EmissiveFactor");
static const QVariant EMISSIVE = QByteArray("Emissive");
static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor");
static const QVariant SHININESS = QByteArray("Shininess");
static const QVariant OPACITY = QByteArray("Opacity");
static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map");
static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color");
static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map");
static const QVariant MAYA_ROUGHNESS = QByteArray("Maya|roughness");
static const QVariant MAYA_USE_ROUGHNESS_MAP = QByteArray("Maya|use_roughness_map");
static const QVariant MAYA_METALLIC = QByteArray("Maya|metallic");
static const QVariant MAYA_USE_METALLIC_MAP = QByteArray("Maya|use_metallic_map");
static const QVariant MAYA_EMISSIVE = QByteArray("Maya|emissive");
static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity");
static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map");
static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map");
static const QVariant MAYA_UV_SCALE = QByteArray("Maya|uv_scale");
static const QVariant MAYA_UV_OFFSET = QByteArray("Maya|uv_offset");
static const QVariant DIFFUSE_COLOR = hifi::ByteArray("DiffuseColor");
static const QVariant DIFFUSE_FACTOR = hifi::ByteArray("DiffuseFactor");
static const QVariant DIFFUSE = hifi::ByteArray("Diffuse");
static const QVariant SPECULAR_COLOR = hifi::ByteArray("SpecularColor");
static const QVariant SPECULAR_FACTOR = hifi::ByteArray("SpecularFactor");
static const QVariant SPECULAR = hifi::ByteArray("Specular");
static const QVariant EMISSIVE_COLOR = hifi::ByteArray("EmissiveColor");
static const QVariant EMISSIVE_FACTOR = hifi::ByteArray("EmissiveFactor");
static const QVariant EMISSIVE = hifi::ByteArray("Emissive");
static const QVariant AMBIENT_FACTOR = hifi::ByteArray("AmbientFactor");
static const QVariant SHININESS = hifi::ByteArray("Shininess");
static const QVariant OPACITY = hifi::ByteArray("Opacity");
static const QVariant MAYA_USE_NORMAL_MAP = hifi::ByteArray("Maya|use_normal_map");
static const QVariant MAYA_BASE_COLOR = hifi::ByteArray("Maya|base_color");
static const QVariant MAYA_USE_COLOR_MAP = hifi::ByteArray("Maya|use_color_map");
static const QVariant MAYA_ROUGHNESS = hifi::ByteArray("Maya|roughness");
static const QVariant MAYA_USE_ROUGHNESS_MAP = hifi::ByteArray("Maya|use_roughness_map");
static const QVariant MAYA_METALLIC = hifi::ByteArray("Maya|metallic");
static const QVariant MAYA_USE_METALLIC_MAP = hifi::ByteArray("Maya|use_metallic_map");
static const QVariant MAYA_EMISSIVE = hifi::ByteArray("Maya|emissive");
static const QVariant MAYA_EMISSIVE_INTENSITY = hifi::ByteArray("Maya|emissive_intensity");
static const QVariant MAYA_USE_EMISSIVE_MAP = hifi::ByteArray("Maya|use_emissive_map");
static const QVariant MAYA_USE_AO_MAP = hifi::ByteArray("Maya|use_ao_map");
static const QVariant MAYA_UV_SCALE = hifi::ByteArray("Maya|uv_scale");
static const QVariant MAYA_UV_OFFSET = hifi::ByteArray("Maya|uv_offset");
static const int MAYA_UV_OFFSET_PROPERTY_LENGTH = 6;
static const int MAYA_UV_SCALE_PROPERTY_LENGTH = 6;
@ -1050,7 +1052,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
}
} else if (object.properties.last() == "BlendShapeChannel") {
QByteArray name = object.properties.at(1).toByteArray();
hifi::ByteArray name = object.properties.at(1).toByteArray();
name = name.left(name.indexOf('\0'));
if (!blendshapeIndices.contains(name)) {
@ -1087,8 +1089,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
#endif
}
} else if (child.name == "Connections") {
static const QVariant OO = QByteArray("OO");
static const QVariant OP = QByteArray("OP");
static const QVariant OO = hifi::ByteArray("OO");
static const QVariant OP = hifi::ByteArray("OP");
foreach (const FBXNode& connection, child.children) {
if (connection.name == "C" || connection.name == "Connect") {
if (connection.properties.at(0) == OO) {
@ -1107,7 +1109,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
}
} else if (connection.properties.at(0) == OP) {
int counter = 0;
QByteArray type = connection.properties.at(3).toByteArray().toLower();
hifi::ByteArray type = connection.properties.at(3).toByteArray().toLower();
if (type.contains("DiffuseFactor")) {
diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1));
} else if ((type.contains("diffuse") && !type.contains("tex_global_diffuse"))) {
@ -1404,9 +1406,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
// look for textures, material properties
// allocate the Part material library
// NOTE: extracted.partMaterialTextures is empty for FBX_DRACO_MESH_VERSION >= 2. In that case, the mesh part's materialID string is already defined.
int materialIndex = 0;
int textureIndex = 0;
bool generateTangents = false;
QList<QString> children = _connectionChildMap.values(modelID);
for (int i = children.size() - 1; i >= 0; i--) {
@ -1419,12 +1421,10 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr
if (extracted.partMaterialTextures.at(j).first == materialIndex) {
HFMMeshPart& part = extracted.mesh.parts[j];
part.materialID = material.materialID;
generateTangents |= material.needTangentSpace();
}
}
materialIndex++;
} else if (_textureFilenames.contains(childID)) {
// NOTE (Sabrina 2019/01/11): getTextures now takes in the materialID as a second parameter, because FBX material nodes can sometimes have uv transform information (ex: "Maya|uv_scale")
// I'm leaving the second parameter blank right now as this code may never be used.
@ -1694,11 +1694,13 @@ std::unique_ptr<hfm::Serializer::Factory> FBXSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<FBXSerializer>>();
}
HFMModel::Pointer FBXSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
QBuffer buffer(const_cast<QByteArray*>(&data));
HFMModel::Pointer FBXSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) {
QBuffer buffer(const_cast<hifi::ByteArray*>(&data));
buffer.open(QIODevice::ReadOnly);
_rootNode = parseFBX(&buffer);
// FBXSerializer's mapping parameter supports the bool "deduplicateIndices," which is passed into FBXSerializer::extractMesh as "deduplicate"
return HFMModel::Pointer(extractHFMModel(mapping, url.toString()));
}

View file

@ -15,9 +15,6 @@
#include <QtGlobal>
#include <QMetaType>
#include <QSet>
#include <QUrl>
#include <QVarLengthArray>
#include <QVariant>
#include <QVector>
#include <glm/glm.hpp>
@ -25,6 +22,7 @@
#include <Extents.h>
#include <Transform.h>
#include <shared/HifiTypes.h>
#include "FBX.h"
#include <hfm/HFMSerializer.h>
@ -114,25 +112,25 @@ public:
HFMModel* _hfmModel;
/// Reads HFMModel from the supplied model and mapping data.
/// \exception QString if an error occurs in parsing
HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override;
HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override;
FBXNode _rootNode;
static FBXNode parseFBX(QIODevice* device);
HFMModel* extractHFMModel(const QVariantHash& mapping, const QString& url);
HFMModel* extractHFMModel(const hifi::VariantHash& mapping, const QString& url);
static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate = true);
static ExtractedMesh extractMesh(const FBXNode& object, unsigned int& meshIndex, bool deduplicate);
QHash<QString, ExtractedMesh> meshes;
HFMTexture getTexture(const QString& textureID, const QString& materialID);
QHash<QString, QString> _textureNames;
// Hashes the original RelativeFilename of textures
QHash<QString, QByteArray> _textureFilepaths;
QHash<QString, hifi::ByteArray> _textureFilepaths;
// Hashes the place to look for textures, in case they are not inlined
QHash<QString, QByteArray> _textureFilenames;
QHash<QString, hifi::ByteArray> _textureFilenames;
// Hashes texture content by filepath, in case they are inlined
QHash<QByteArray, QByteArray> _textureContent;
QHash<hifi::ByteArray, hifi::ByteArray> _textureContent;
QHash<QString, TextureParam> _textureParams;

View file

@ -15,7 +15,6 @@
#include <memory>
#include <QBuffer>
#include <QDataStream>
#include <QIODevice>
#include <QStringList>
#include <QTextStream>
@ -29,7 +28,7 @@
HFMTexture FBXSerializer::getTexture(const QString& textureID, const QString& materialID) {
HFMTexture texture;
const QByteArray& filepath = _textureFilepaths.value(textureID);
const hifi::ByteArray& filepath = _textureFilepaths.value(textureID);
texture.content = _textureContent.value(filepath);
if (texture.content.isEmpty()) { // the content is not inlined

View file

@ -13,12 +13,20 @@
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
// gcc and clang
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-compare"
#endif
#include <draco/compression/decode.h>
#ifdef _WIN32
#pragma warning( pop )
#endif
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#include <iostream>
#include <QBuffer>
@ -190,8 +198,8 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me
bool isMaterialPerPolygon = false;
static const QVariant BY_VERTICE = QByteArray("ByVertice");
static const QVariant INDEX_TO_DIRECT = QByteArray("IndexToDirect");
static const QVariant BY_VERTICE = hifi::ByteArray("ByVertice");
static const QVariant INDEX_TO_DIRECT = hifi::ByteArray("IndexToDirect");
bool isDracoMesh = false;
@ -321,7 +329,7 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me
}
}
} else if (child.name == "LayerElementMaterial") {
static const QVariant BY_POLYGON = QByteArray("ByPolygon");
static const QVariant BY_POLYGON = hifi::ByteArray("ByPolygon");
foreach (const FBXNode& subdata, child.children) {
if (subdata.name == "Materials") {
materials = getIntVector(subdata);
@ -345,10 +353,26 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me
isDracoMesh = true;
data.extracted.mesh.wasCompressed = true;
// Check for additional metadata
unsigned int dracoMeshNodeVersion = 1;
std::vector<QString> dracoMaterialList;
for (const auto& dracoChild : child.children) {
if (dracoChild.name == "FBXDracoMeshVersion") {
if (!dracoChild.children.isEmpty()) {
dracoMeshNodeVersion = dracoChild.properties[0].toUInt();
}
} else if (dracoChild.name == "MaterialList") {
dracoMaterialList.reserve(dracoChild.properties.size());
for (const auto& materialID : dracoChild.properties) {
dracoMaterialList.push_back(materialID.toString());
}
}
}
// load the draco mesh from the FBX and create a draco::Mesh
draco::Decoder decoder;
draco::DecoderBuffer decodedBuffer;
QByteArray dracoArray = child.properties.at(0).value<QByteArray>();
hifi::ByteArray dracoArray = child.properties.at(0).value<hifi::ByteArray>();
decodedBuffer.Init(dracoArray.data(), dracoArray.size());
std::unique_ptr<draco::Mesh> dracoMesh(new draco::Mesh());
@ -462,8 +486,20 @@ ExtractedMesh FBXSerializer::extractMesh(const FBXNode& object, unsigned int& me
// grab or setup the HFMMeshPart for the part this face belongs to
int& partIndexPlusOne = materialTextureParts[materialTexture];
if (partIndexPlusOne == 0) {
data.extracted.partMaterialTextures.append(materialTexture);
data.extracted.mesh.parts.resize(data.extracted.mesh.parts.size() + 1);
HFMMeshPart& part = data.extracted.mesh.parts.back();
// Figure out what material this part is
if (dracoMeshNodeVersion >= 2) {
// Define the materialID now
if (dracoMaterialList.size() - 1 <= materialID) {
part.materialID = dracoMaterialList[materialID];
}
} else {
// Define the materialID later, based on the order of first appearance of the materials in the _connectionChildMap
data.extracted.partMaterialTextures.append(materialTexture);
}
partIndexPlusOne = data.extracted.mesh.parts.size();
}

View file

@ -48,10 +48,10 @@ QVariant readBinaryArray(QDataStream& in, int& position) {
QVector<T> values;
if ((int)QSysInfo::ByteOrder == (int)in.byteOrder()) {
values.resize(arrayLength);
QByteArray arrayData;
hifi::ByteArray arrayData;
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
// preface encoded data with uncompressed length
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0);
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
position += compressedLength;
@ -73,11 +73,11 @@ QVariant readBinaryArray(QDataStream& in, int& position) {
values.reserve(arrayLength);
if (encoding == FBX_PROPERTY_COMPRESSED_FLAG) {
// preface encoded data with uncompressed length
QByteArray compressed(sizeof(quint32) + compressedLength, 0);
hifi::ByteArray compressed(sizeof(quint32) + compressedLength, 0);
*((quint32*)compressed.data()) = qToBigEndian<quint32>(arrayLength * sizeof(T));
in.readRawData(compressed.data() + sizeof(quint32), compressedLength);
position += compressedLength;
QByteArray uncompressed = qUncompress(compressed);
hifi::ByteArray uncompressed = qUncompress(compressed);
if (uncompressed.isEmpty()) { // answers empty byte array if corrupt
throw QString("corrupt fbx file");
}
@ -234,7 +234,7 @@ public:
};
int nextToken();
const QByteArray& getDatum() const { return _datum; }
const hifi::ByteArray& getDatum() const { return _datum; }
void pushBackToken(int token) { _pushedBackToken = token; }
void ungetChar(char ch) { _device->ungetChar(ch); }
@ -242,7 +242,7 @@ public:
private:
QIODevice* _device;
QByteArray _datum;
hifi::ByteArray _datum;
int _pushedBackToken;
};
@ -325,7 +325,7 @@ FBXNode parseTextFBXNode(Tokenizer& tokenizer) {
expectingDatum = true;
} else if (token == Tokenizer::DATUM_TOKEN && expectingDatum) {
QByteArray datum = tokenizer.getDatum();
hifi::ByteArray datum = tokenizer.getDatum();
if ((token = tokenizer.nextToken()) == ':') {
tokenizer.ungetChar(':');
tokenizer.pushBackToken(Tokenizer::DATUM_TOKEN);

View file

@ -15,6 +15,8 @@
#include <QBuffer>
#include <QVariantHash>
static const unsigned int FST_VERSION = 1;
static const QString FST_VERSION_FIELD = "version";
static const QString NAME_FIELD = "name";
static const QString TYPE_FIELD = "type";
static const QString FILENAME_FIELD = "filename";

View file

@ -125,18 +125,18 @@ bool GLTFSerializer::getObjectArrayVal(const QJsonObject& object, const QString&
return _defined;
}
QByteArray GLTFSerializer::setGLBChunks(const QByteArray& data) {
hifi::ByteArray GLTFSerializer::setGLBChunks(const hifi::ByteArray& data) {
int byte = 4;
int jsonStart = data.indexOf("JSON", Qt::CaseSensitive);
int binStart = data.indexOf("BIN", Qt::CaseSensitive);
int jsonLength, binLength;
QByteArray jsonLengthChunk, binLengthChunk;
hifi::ByteArray jsonLengthChunk, binLengthChunk;
jsonLengthChunk = data.mid(jsonStart - byte, byte);
QDataStream tempJsonLen(jsonLengthChunk);
tempJsonLen.setByteOrder(QDataStream::LittleEndian);
tempJsonLen >> jsonLength;
QByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength);
hifi::ByteArray jsonChunk = data.mid(jsonStart + byte, jsonLength);
if (binStart != -1) {
binLengthChunk = data.mid(binStart - byte, byte);
@ -567,10 +567,10 @@ bool GLTFSerializer::addTexture(const QJsonObject& object) {
return true;
}
bool GLTFSerializer::parseGLTF(const QByteArray& data) {
bool GLTFSerializer::parseGLTF(const hifi::ByteArray& data) {
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);
QByteArray jsonChunk = data;
hifi::ByteArray jsonChunk = data;
if (_url.toString().endsWith("glb") && data.indexOf("glTF") == 0 && data.contains("JSON")) {
jsonChunk = setGLBChunks(data);
@ -734,7 +734,7 @@ glm::mat4 GLTFSerializer::getModelTransform(const GLTFNode& node) {
return tmat;
}
bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const QUrl& url) {
bool GLTFSerializer::buildGeometry(HFMModel& hfmModel, const hifi::URL& url) {
//Build dependencies
QVector<QVector<int>> nodeDependencies(_file.nodes.size());
@ -994,15 +994,15 @@ std::unique_ptr<hfm::Serializer::Factory> GLTFSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<GLTFSerializer>>();
}
HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
HFMModel::Pointer GLTFSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) {
_url = url;
// Normalize url for local files
QUrl normalizeUrl = DependencyManager::get<ResourceManager>()->normalizeURL(_url);
hifi::URL normalizeUrl = DependencyManager::get<ResourceManager>()->normalizeURL(_url);
if (normalizeUrl.scheme().isEmpty() || (normalizeUrl.scheme() == "file")) {
QString localFileName = PathUtils::expandToLocalDataAbsolutePath(normalizeUrl).toLocalFile();
_url = QUrl(QFileInfo(localFileName).absoluteFilePath());
_url = hifi::URL(QFileInfo(localFileName).absoluteFilePath());
}
if (parseGLTF(data)) {
@ -1020,15 +1020,15 @@ HFMModel::Pointer GLTFSerializer::read(const QByteArray& data, const QVariantHas
return nullptr;
}
bool GLTFSerializer::readBinary(const QString& url, QByteArray& outdata) {
bool GLTFSerializer::readBinary(const QString& url, hifi::ByteArray& outdata) {
bool success;
if (url.contains("data:application/octet-stream;base64,")) {
outdata = requestEmbeddedData(url);
success = !outdata.isEmpty();
} else {
QUrl binaryUrl = _url.resolved(url);
std::tie<bool, QByteArray>(success, outdata) = requestData(binaryUrl);
hifi::URL binaryUrl = _url.resolved(url);
std::tie<bool, hifi::ByteArray>(success, outdata) = requestData(binaryUrl);
}
return success;
@ -1038,16 +1038,16 @@ bool GLTFSerializer::doesResourceExist(const QString& url) {
if (_url.isEmpty()) {
return false;
}
QUrl candidateUrl = _url.resolved(url);
hifi::URL candidateUrl = _url.resolved(url);
return DependencyManager::get<ResourceManager>()->resourceExists(candidateUrl);
}
std::tuple<bool, QByteArray> GLTFSerializer::requestData(QUrl& url) {
std::tuple<bool, hifi::ByteArray> GLTFSerializer::requestData(hifi::URL& url) {
auto request = DependencyManager::get<ResourceManager>()->createResourceRequest(
nullptr, url, true, -1, "GLTFSerializer::requestData");
if (!request) {
return std::make_tuple(false, QByteArray());
return std::make_tuple(false, hifi::ByteArray());
}
QEventLoop loop;
@ -1058,17 +1058,17 @@ std::tuple<bool, QByteArray> GLTFSerializer::requestData(QUrl& url) {
if (request->getResult() == ResourceRequest::Success) {
return std::make_tuple(true, request->getData());
} else {
return std::make_tuple(false, QByteArray());
return std::make_tuple(false, hifi::ByteArray());
}
}
QByteArray GLTFSerializer::requestEmbeddedData(const QString& url) {
hifi::ByteArray GLTFSerializer::requestEmbeddedData(const QString& url) {
QString binaryUrl = url.split(",")[1];
return binaryUrl.isEmpty() ? QByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8());
return binaryUrl.isEmpty() ? hifi::ByteArray() : QByteArray::fromBase64(binaryUrl.toUtf8());
}
QNetworkReply* GLTFSerializer::request(QUrl& url, bool isTest) {
QNetworkReply* GLTFSerializer::request(hifi::URL& url, bool isTest) {
if (!qApp) {
return nullptr;
}
@ -1099,8 +1099,8 @@ HFMTexture GLTFSerializer::getHFMTexture(const GLTFTexture& texture) {
if (texture.defined["source"]) {
QString url = _file.images[texture.source].uri;
QString fname = QUrl(url).fileName();
QUrl textureUrl = _url.resolved(url);
QString fname = hifi::URL(url).fileName();
hifi::URL textureUrl = _url.resolved(url);
qCDebug(modelformat) << "fname: " << fname;
fbxtex.name = fname;
fbxtex.filename = textureUrl.toEncoded();
@ -1188,7 +1188,7 @@ void GLTFSerializer::setHFMMaterial(HFMMaterial& fbxmat, const GLTFMaterial& mat
}
template<typename T, typename L>
bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count,
bool GLTFSerializer::readArray(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<L>& outarray, int accessorType) {
QDataStream blobstream(bin);
@ -1245,7 +1245,7 @@ bool GLTFSerializer::readArray(const QByteArray& bin, int byteOffset, int count,
return true;
}
template<typename T>
bool GLTFSerializer::addArrayOfType(const QByteArray& bin, int byteOffset, int count,
bool GLTFSerializer::addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<T>& outarray, int accessorType, int componentType) {
switch (componentType) {

View file

@ -214,7 +214,7 @@ struct GLTFBufferView {
struct GLTFBuffer {
int byteLength; //required
QString uri;
QByteArray blob;
hifi::ByteArray blob;
QMap<QString, bool> defined;
void dump() {
if (defined["byteLength"]) {
@ -705,16 +705,16 @@ public:
MediaType getMediaType() const override;
std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override;
HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override;
private:
GLTFFile _file;
QUrl _url;
QByteArray _glbBinary;
hifi::URL _url;
hifi::ByteArray _glbBinary;
glm::mat4 getModelTransform(const GLTFNode& node);
bool buildGeometry(HFMModel& hfmModel, const QUrl& url);
bool parseGLTF(const QByteArray& data);
bool buildGeometry(HFMModel& hfmModel, const hifi::URL& url);
bool parseGLTF(const hifi::ByteArray& data);
bool getStringVal(const QJsonObject& object, const QString& fieldname,
QString& value, QMap<QString, bool>& defined);
@ -733,7 +733,7 @@ private:
bool getObjectArrayVal(const QJsonObject& object, const QString& fieldname,
QJsonArray& objects, QMap<QString, bool>& defined);
QByteArray setGLBChunks(const QByteArray& data);
hifi::ByteArray setGLBChunks(const hifi::ByteArray& data);
int getMaterialAlphaMode(const QString& type);
int getAccessorType(const QString& type);
@ -760,24 +760,24 @@ private:
bool addSkin(const QJsonObject& object);
bool addTexture(const QJsonObject& object);
bool readBinary(const QString& url, QByteArray& outdata);
bool readBinary(const QString& url, hifi::ByteArray& outdata);
template<typename T, typename L>
bool readArray(const QByteArray& bin, int byteOffset, int count,
bool readArray(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<L>& outarray, int accessorType);
template<typename T>
bool addArrayOfType(const QByteArray& bin, int byteOffset, int count,
bool addArrayOfType(const hifi::ByteArray& bin, int byteOffset, int count,
QVector<T>& outarray, int accessorType, int componentType);
void retriangulate(const QVector<int>& in_indices, const QVector<glm::vec3>& in_vertices,
const QVector<glm::vec3>& in_normals, QVector<int>& out_indices,
QVector<glm::vec3>& out_vertices, QVector<glm::vec3>& out_normals);
std::tuple<bool, QByteArray> requestData(QUrl& url);
QByteArray requestEmbeddedData(const QString& url);
std::tuple<bool, hifi::ByteArray> requestData(hifi::URL& url);
hifi::ByteArray requestEmbeddedData(const QString& url);
QNetworkReply* request(QUrl& url, bool isTest);
QNetworkReply* request(hifi::URL& url, bool isTest);
bool doesResourceExist(const QString& url);

View file

@ -54,7 +54,7 @@ T& checked_at(QVector<T>& vector, int i) {
OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) {
}
const QByteArray OBJTokenizer::getLineAsDatum() {
const hifi::ByteArray OBJTokenizer::getLineAsDatum() {
return _device->readLine().trimmed();
}
@ -117,7 +117,7 @@ bool OBJTokenizer::isNextTokenFloat() {
if (nextToken() != OBJTokenizer::DATUM_TOKEN) {
return false;
}
QByteArray token = getDatum();
hifi::ByteArray token = getDatum();
pushBackToken(OBJTokenizer::DATUM_TOKEN);
bool ok;
token.toFloat(&ok);
@ -182,7 +182,7 @@ void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID) {
// OBJFace
// NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just
// pairing it with the vertices vector for consistency.
bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& vertexColors) {
bool OBJFace::add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& vertexColors) {
bool ok;
int index = vertexIndex.toInt(&ok);
if (!ok) {
@ -238,11 +238,11 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f
}
}
bool OBJSerializer::isValidTexture(const QByteArray &filename) {
bool OBJSerializer::isValidTexture(const hifi::ByteArray &filename) {
if (_url.isEmpty()) {
return false;
}
QUrl candidateUrl = _url.resolved(QUrl(filename));
hifi::URL candidateUrl = _url.resolved(hifi::URL(filename));
return DependencyManager::get<ResourceManager>()->resourceExists(candidateUrl);
}
@ -278,7 +278,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) {
#endif
return;
}
QByteArray token = tokenizer.getDatum();
hifi::ByteArray token = tokenizer.getDatum();
if (token == "newmtl") {
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
return;
@ -328,8 +328,8 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) {
} else if (token == "Ks") {
currentMaterial.specularColor = tokenizer.getVec3();
} else if ((token == "map_Kd") || (token == "map_Ke") || (token == "map_Ks") || (token == "map_bump") || (token == "bump") || (token == "map_d")) {
const QByteArray textureLine = tokenizer.getLineAsDatum();
QByteArray filename;
const hifi::ByteArray textureLine = tokenizer.getLineAsDatum();
hifi::ByteArray filename;
OBJMaterialTextureOptions textureOptions;
parseTextureLine(textureLine, filename, textureOptions);
if (filename.endsWith(".tga")) {
@ -354,7 +354,7 @@ void OBJSerializer::parseMaterialLibrary(QIODevice* device) {
}
}
void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions) {
void OBJSerializer::parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions) {
// Texture options reference http://paulbourke.net/dataformats/mtl/
// and https://wikivisually.com/wiki/Material_Template_Library
@ -442,12 +442,12 @@ void OBJSerializer::parseTextureLine(const QByteArray& textureLine, QByteArray&
}
}
std::tuple<bool, QByteArray> requestData(QUrl& url) {
std::tuple<bool, hifi::ByteArray> requestData(hifi::URL& url) {
auto request = DependencyManager::get<ResourceManager>()->createResourceRequest(
nullptr, url, true, -1, "(OBJSerializer) requestData");
if (!request) {
return std::make_tuple(false, QByteArray());
return std::make_tuple(false, hifi::ByteArray());
}
QEventLoop loop;
@ -458,12 +458,12 @@ std::tuple<bool, QByteArray> requestData(QUrl& url) {
if (request->getResult() == ResourceRequest::Success) {
return std::make_tuple(true, request->getData());
} else {
return std::make_tuple(false, QByteArray());
return std::make_tuple(false, hifi::ByteArray());
}
}
QNetworkReply* request(QUrl& url, bool isTest) {
QNetworkReply* request(hifi::URL& url, bool isTest) {
if (!qApp) {
return nullptr;
}
@ -488,7 +488,7 @@ QNetworkReply* request(QUrl& url, bool isTest) {
}
bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel,
bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel,
float& scaleGuess, bool combineParts) {
FaceGroup faces;
HFMMesh& mesh = hfmModel.meshes[0];
@ -522,7 +522,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m
result = false;
break;
}
QByteArray token = tokenizer.getDatum();
hifi::ByteArray token = tokenizer.getDatum();
//qCDebug(modelformat) << token;
// we don't support separate objects in the same file, so treat "o" the same as "g".
if (token == "g" || token == "o") {
@ -535,7 +535,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m
if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {
break;
}
QByteArray groupName = tokenizer.getDatum();
hifi::ByteArray groupName = tokenizer.getDatum();
currentGroup = groupName;
if (!combineParts) {
currentMaterialName = QString("part-") + QString::number(_partCounter++);
@ -544,7 +544,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m
if (tokenizer.nextToken(true) != OBJTokenizer::DATUM_TOKEN) {
break;
}
QByteArray libraryName = tokenizer.getDatum();
hifi::ByteArray libraryName = tokenizer.getDatum();
librariesSeen[libraryName] = true;
// We'll read it later only if we actually need it.
} else if (token == "usemtl") {
@ -598,14 +598,14 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m
// vertex-index
// vertex-index/texture-index
// vertex-index/texture-index/surface-normal-index
QByteArray token = tokenizer.getDatum();
hifi::ByteArray token = tokenizer.getDatum();
auto firstChar = token[0];
// Tokenizer treats line endings as whitespace. Non-digit and non-negative sign indicates done;
if (!isdigit(firstChar) && firstChar != '-') {
tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN);
break;
}
QList<QByteArray> parts = token.split('/');
QList<hifi::ByteArray> parts = token.split('/');
assert(parts.count() >= 1);
assert(parts.count() <= 3);
// If indices are negative relative indices then adjust them to absolute indices based on current vector sizes
@ -626,7 +626,7 @@ bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& m
}
}
}
const QByteArray noData {};
const hifi::ByteArray noData {};
face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData,
vertices, vertexColors);
face.groupName = currentGroup;
@ -661,9 +661,9 @@ std::unique_ptr<hfm::Serializer::Factory> OBJSerializer::getFactory() const {
return std::make_unique<hfm::Serializer::SimpleFactory<OBJSerializer>>();
}
HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url) {
HFMModel::Pointer OBJSerializer::read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url) {
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);
QBuffer buffer { const_cast<QByteArray*>(&data) };
QBuffer buffer { const_cast<hifi::ByteArray*>(&data) };
buffer.open(QIODevice::ReadOnly);
auto hfmModelPtr = std::make_shared<HFMModel>();
@ -849,11 +849,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash
int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail
QString basename = filename.remove(extIndex + 1, sizeof("obj"));
preDefinedMaterial.diffuseColor = glm::vec3(1.0f);
QVector<QByteArray> extensions = { "jpg", "jpeg", "png", "tga" };
QByteArray base = basename.toUtf8(), textName = "";
QVector<hifi::ByteArray> extensions = { "jpg", "jpeg", "png", "tga" };
hifi::ByteArray base = basename.toUtf8(), textName = "";
qCDebug(modelformat) << "OBJSerializer looking for default texture";
for (int i = 0; i < extensions.count(); i++) {
QByteArray candidateString = base + extensions[i];
hifi::ByteArray candidateString = base + extensions[i];
if (isValidTexture(candidateString)) {
textName = candidateString;
break;
@ -871,11 +871,11 @@ HFMModel::Pointer OBJSerializer::read(const QByteArray& data, const QVariantHash
if (needsMaterialLibrary) {
foreach (QString libraryName, librariesSeen.keys()) {
// Throw away any path part of libraryName, and merge against original url.
QUrl libraryUrl = _url.resolved(QUrl(libraryName).fileName());
hifi::URL libraryUrl = _url.resolved(hifi::URL(libraryName).fileName());
qCDebug(modelformat) << "OBJSerializer material library" << libraryName;
bool success;
QByteArray data;
std::tie<bool, QByteArray>(success, data) = requestData(libraryUrl);
hifi::ByteArray data;
std::tie<bool, hifi::ByteArray>(success, data) = requestData(libraryUrl);
if (success) {
QBuffer buffer { &data };
buffer.open(QIODevice::ReadOnly);

View file

@ -25,9 +25,9 @@ public:
COMMENT_TOKEN = 0x101
};
int nextToken(bool allowSpaceChar = false);
const QByteArray& getDatum() const { return _datum; }
const hifi::ByteArray& getDatum() const { return _datum; }
bool isNextTokenFloat();
const QByteArray getLineAsDatum(); // some "filenames" have spaces in them
const hifi::ByteArray getLineAsDatum(); // some "filenames" have spaces in them
void skipLine() { _device->readLine(); }
void pushBackToken(int token) { _pushedBackToken = token; }
void ungetChar(char ch) { _device->ungetChar(ch); }
@ -39,7 +39,7 @@ public:
private:
QIODevice* _device;
QByteArray _datum;
hifi::ByteArray _datum;
int _pushedBackToken;
QString _comment;
};
@ -52,7 +52,7 @@ public:
QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use.
QString materialName;
// Add one more set of vertex data. Answers true if successful
bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex,
bool add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex,
const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& vertexColors);
// Return a set of one or more OBJFaces from this one, in which each is just a triangle.
// Even though HFMMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles.
@ -75,11 +75,11 @@ public:
glm::vec3 diffuseColor;
glm::vec3 specularColor;
glm::vec3 emissiveColor;
QByteArray diffuseTextureFilename;
QByteArray specularTextureFilename;
QByteArray emissiveTextureFilename;
QByteArray bumpTextureFilename;
QByteArray opacityTextureFilename;
hifi::ByteArray diffuseTextureFilename;
hifi::ByteArray specularTextureFilename;
hifi::ByteArray emissiveTextureFilename;
hifi::ByteArray bumpTextureFilename;
hifi::ByteArray opacityTextureFilename;
OBJMaterialTextureOptions bumpTextureOptions;
int illuminationModel;
@ -103,17 +103,17 @@ public:
QString currentMaterialName;
QHash<QString, OBJMaterial> materials;
HFMModel::Pointer read(const QByteArray& data, const QVariantHash& mapping, const QUrl& url = QUrl()) override;
HFMModel::Pointer read(const hifi::ByteArray& data, const hifi::VariantHash& mapping, const hifi::URL& url = hifi::URL()) override;
private:
QUrl _url;
hifi::URL _url;
QHash<QByteArray, bool> librariesSeen;
bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, HFMModel& hfmModel,
QHash<hifi::ByteArray, bool> librariesSeen;
bool parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel,
float& scaleGuess, bool combineParts);
void parseMaterialLibrary(QIODevice* device);
void parseTextureLine(const QByteArray& textureLine, QByteArray& filename, OBJMaterialTextureOptions& textureOptions);
bool isValidTexture(const QByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format.
void parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions);
bool isValidTexture(const hifi::ByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format.
int _partCounter { 0 };
};

View file

@ -96,6 +96,8 @@ namespace scriptable {
bool defaultFallthrough;
std::unordered_map<uint, bool> propertyFallthroughs; // not actually exposed to script
graphics::MaterialKey key { 0 };
};
/**jsdoc

View file

@ -363,24 +363,87 @@ namespace scriptable {
obj.setProperty("name", material.name);
obj.setProperty("model", material.model);
const QScriptValue FALLTHROUGH("fallthrough");
obj.setProperty("opacity", material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT) ? FALLTHROUGH : material.opacity);
obj.setProperty("roughness", material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT) ? FALLTHROUGH : material.roughness);
obj.setProperty("metallic", material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT) ? FALLTHROUGH : material.metallic);
obj.setProperty("scattering", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT) ? FALLTHROUGH : material.scattering);
obj.setProperty("unlit", material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT) ? FALLTHROUGH : material.unlit);
obj.setProperty("emissive", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.emissive));
obj.setProperty("albedo", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT) ? FALLTHROUGH : vec3ColorToScriptValue(engine, material.albedo));
bool hasPropertyFallthroughs = !material.propertyFallthroughs.empty();
obj.setProperty("emissiveMap", material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT) ? FALLTHROUGH : material.emissiveMap);
obj.setProperty("albedoMap", material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT) ? FALLTHROUGH : material.albedoMap);
obj.setProperty("opacityMap", material.opacityMap);
obj.setProperty("occlusionMap", material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT) ? FALLTHROUGH : material.occlusionMap);
obj.setProperty("lightmapMap", material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT) ? FALLTHROUGH : material.lightmapMap);
obj.setProperty("scatteringMap", material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT) ? FALLTHROUGH : material.scatteringMap);
const QScriptValue FALLTHROUGH("fallthrough");
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OPACITY_VAL_BIT)) {
obj.setProperty("opacity", FALLTHROUGH);
} else if (material.key.isTranslucentFactor()) {
obj.setProperty("opacity", material.opacity);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::GLOSSY_VAL_BIT)) {
obj.setProperty("roughness", FALLTHROUGH);
} else if (material.key.isGlossy()) {
obj.setProperty("roughness", material.roughness);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_VAL_BIT)) {
obj.setProperty("metallic", FALLTHROUGH);
} else if (material.key.isMetallic()) {
obj.setProperty("metallic", material.metallic);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_VAL_BIT)) {
obj.setProperty("scattering", FALLTHROUGH);
} else if (material.key.isScattering()) {
obj.setProperty("scattering", material.scattering);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::UNLIT_VAL_BIT)) {
obj.setProperty("unlit", FALLTHROUGH);
} else if (material.key.isUnlit()) {
obj.setProperty("unlit", material.unlit);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_VAL_BIT)) {
obj.setProperty("emissive", FALLTHROUGH);
} else if (material.key.isEmissive()) {
obj.setProperty("emissive", vec3ColorToScriptValue(engine, material.emissive));
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_VAL_BIT)) {
obj.setProperty("albedo", FALLTHROUGH);
} else if (material.key.isAlbedo()) {
obj.setProperty("albedo", vec3ColorToScriptValue(engine, material.albedo));
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::EMISSIVE_MAP_BIT)) {
obj.setProperty("emissiveMap", FALLTHROUGH);
} else if (!material.emissiveMap.isEmpty()) {
obj.setProperty("emissiveMap", material.emissiveMap);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ALBEDO_MAP_BIT)) {
obj.setProperty("albedoMap", FALLTHROUGH);
} else if (!material.albedoMap.isEmpty()) {
obj.setProperty("albedoMap", material.albedoMap);
}
if (!material.opacityMap.isEmpty()) {
obj.setProperty("opacityMap", material.opacityMap);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::OCCLUSION_MAP_BIT)) {
obj.setProperty("occlusionMap", FALLTHROUGH);
} else if (!material.occlusionMap.isEmpty()) {
obj.setProperty("occlusionMap", material.occlusionMap);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::LIGHTMAP_MAP_BIT)) {
obj.setProperty("lightmapMap", FALLTHROUGH);
} else if (!material.lightmapMap.isEmpty()) {
obj.setProperty("lightmapMap", material.lightmapMap);
}
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::SCATTERING_MAP_BIT)) {
obj.setProperty("scatteringMap", FALLTHROUGH);
} else if (!material.scatteringMap.isEmpty()) {
obj.setProperty("scatteringMap", material.scatteringMap);
}
// Only set one of each of these
if (material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::METALLIC_MAP_BIT)) {
obj.setProperty("metallicMap", FALLTHROUGH);
} else if (!material.metallicMap.isEmpty()) {
obj.setProperty("metallicMap", material.metallicMap);
@ -388,7 +451,7 @@ namespace scriptable {
obj.setProperty("specularMap", material.specularMap);
}
if (material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::ROUGHNESS_MAP_BIT)) {
obj.setProperty("roughnessMap", FALLTHROUGH);
} else if (!material.roughnessMap.isEmpty()) {
obj.setProperty("roughnessMap", material.roughnessMap);
@ -396,7 +459,7 @@ namespace scriptable {
obj.setProperty("glossMap", material.glossMap);
}
if (material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::MaterialKey::NORMAL_MAP_BIT)) {
obj.setProperty("normalMap", FALLTHROUGH);
} else if (!material.normalMap.isEmpty()) {
obj.setProperty("normalMap", material.normalMap);
@ -405,16 +468,16 @@ namespace scriptable {
}
// These need to be implemented, but set the fallthrough for now
if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM0)) {
obj.setProperty("texCoordTransform0", FALLTHROUGH);
}
if (material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::TEXCOORDTRANSFORM1)) {
obj.setProperty("texCoordTransform1", FALLTHROUGH);
}
if (material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::LIGHTMAP_PARAMS)) {
obj.setProperty("lightmapParams", FALLTHROUGH);
}
if (material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) {
if (hasPropertyFallthroughs && material.propertyFallthroughs.at(graphics::Material::MATERIAL_PARAMS)) {
obj.setProperty("materialParams", FALLTHROUGH);
}

View file

@ -103,6 +103,10 @@ private:
};
namespace scriptable {
QScriptValue scriptableMaterialToScriptValue(QScriptEngine* engine, const scriptable::ScriptableMaterial &material);
};
Q_DECLARE_METATYPE(glm::uint32)
Q_DECLARE_METATYPE(QVector<glm::uint32>)
Q_DECLARE_METATYPE(NestableType)

View file

@ -45,75 +45,80 @@ scriptable::ScriptableMaterial& scriptable::ScriptableMaterial::operator=(const
defaultFallthrough = material.defaultFallthrough;
propertyFallthroughs = material.propertyFallthroughs;
key = material.key;
return *this;
}
scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) :
name(material->getName().c_str()),
model(material->getModel().c_str()),
opacity(material->getOpacity()),
roughness(material->getRoughness()),
metallic(material->getMetallic()),
scattering(material->getScattering()),
unlit(material->isUnlit()),
emissive(material->getEmissive()),
albedo(material->getAlbedo()),
defaultFallthrough(material->getDefaultFallthrough()),
propertyFallthroughs(material->getPropertyFallthroughs())
{
auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP);
if (map && map->getTextureSource()) {
emissiveMap = map->getTextureSource()->getUrl().toString();
}
scriptable::ScriptableMaterial::ScriptableMaterial(const graphics::MaterialPointer& material) {
if (material) {
name = material->getName().c_str();
model = material->getModel().c_str();
opacity = material->getOpacity();
roughness = material->getRoughness();
metallic = material->getMetallic();
scattering = material->getScattering();
unlit = material->isUnlit();
emissive = material->getEmissive();
albedo = material->getAlbedo();
defaultFallthrough = material->getDefaultFallthrough();
propertyFallthroughs = material->getPropertyFallthroughs();
key = material->getKey();
map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP);
if (map && map->getTextureSource()) {
albedoMap = map->getTextureSource()->getUrl().toString();
if (map->useAlphaChannel()) {
opacityMap = albedoMap;
auto map = material->getTextureMap(graphics::Material::MapChannel::EMISSIVE_MAP);
if (map && map->getTextureSource()) {
emissiveMap = map->getTextureSource()->getUrl().toString();
}
}
map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP);
if (map && map->getTextureSource()) {
if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) {
metallicMap = map->getTextureSource()->getUrl().toString();
} else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) {
specularMap = map->getTextureSource()->getUrl().toString();
map = material->getTextureMap(graphics::Material::MapChannel::ALBEDO_MAP);
if (map && map->getTextureSource()) {
albedoMap = map->getTextureSource()->getUrl().toString();
if (map->useAlphaChannel()) {
opacityMap = albedoMap;
}
}
}
map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP);
if (map && map->getTextureSource()) {
if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) {
roughnessMap = map->getTextureSource()->getUrl().toString();
} else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) {
glossMap = map->getTextureSource()->getUrl().toString();
map = material->getTextureMap(graphics::Material::MapChannel::METALLIC_MAP);
if (map && map->getTextureSource()) {
if (map->getTextureSource()->getType() == image::TextureUsage::Type::METALLIC_TEXTURE) {
metallicMap = map->getTextureSource()->getUrl().toString();
} else if (map->getTextureSource()->getType() == image::TextureUsage::Type::SPECULAR_TEXTURE) {
specularMap = map->getTextureSource()->getUrl().toString();
}
}
}
map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP);
if (map && map->getTextureSource()) {
if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) {
normalMap = map->getTextureSource()->getUrl().toString();
} else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) {
bumpMap = map->getTextureSource()->getUrl().toString();
map = material->getTextureMap(graphics::Material::MapChannel::ROUGHNESS_MAP);
if (map && map->getTextureSource()) {
if (map->getTextureSource()->getType() == image::TextureUsage::Type::ROUGHNESS_TEXTURE) {
roughnessMap = map->getTextureSource()->getUrl().toString();
} else if (map->getTextureSource()->getType() == image::TextureUsage::Type::GLOSS_TEXTURE) {
glossMap = map->getTextureSource()->getUrl().toString();
}
}
}
map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP);
if (map && map->getTextureSource()) {
occlusionMap = map->getTextureSource()->getUrl().toString();
}
map = material->getTextureMap(graphics::Material::MapChannel::NORMAL_MAP);
if (map && map->getTextureSource()) {
if (map->getTextureSource()->getType() == image::TextureUsage::Type::NORMAL_TEXTURE) {
normalMap = map->getTextureSource()->getUrl().toString();
} else if (map->getTextureSource()->getType() == image::TextureUsage::Type::BUMP_TEXTURE) {
bumpMap = map->getTextureSource()->getUrl().toString();
}
}
map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP);
if (map && map->getTextureSource()) {
lightmapMap = map->getTextureSource()->getUrl().toString();
}
map = material->getTextureMap(graphics::Material::MapChannel::OCCLUSION_MAP);
if (map && map->getTextureSource()) {
occlusionMap = map->getTextureSource()->getUrl().toString();
}
map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP);
if (map && map->getTextureSource()) {
scatteringMap = map->getTextureSource()->getUrl().toString();
map = material->getTextureMap(graphics::Material::MapChannel::LIGHTMAP_MAP);
if (map && map->getTextureSource()) {
lightmapMap = map->getTextureSource()->getUrl().toString();
}
map = material->getTextureMap(graphics::Material::MapChannel::SCATTERING_MAP);
if (map && map->getTextureSource()) {
scatteringMap = map->getTextureSource()->getUrl().toString();
}
}
}

View file

@ -53,6 +53,14 @@ using ColorType = glm::vec3;
const int MAX_NUM_PIXELS_FOR_FBX_TEXTURE = 2048 * 2048;
// The version of the Draco mesh binary data itself. See also: FBX_DRACO_MESH_VERSION in FBX.h
static const int DRACO_MESH_VERSION = 2;
static const int DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES = 1000;
static const int DRACO_ATTRIBUTE_MATERIAL_ID = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES;
static const int DRACO_ATTRIBUTE_TEX_COORD_1 = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 1;
static const int DRACO_ATTRIBUTE_ORIGINAL_INDEX = DRACO_BEGIN_CUSTOM_HIFI_ATTRIBUTES + 2;
// High Fidelity Model namespace
namespace hfm {

View file

@ -205,7 +205,11 @@ QImage processRawImageData(QIODevice& content, const std::string& filename) {
// Help the QImage loader by extracting the image file format from the url filename ext.
// Some tga are not created properly without it.
auto filenameExtension = filename.substr(filename.find_last_of('.') + 1);
content.open(QIODevice::ReadOnly);
if (!content.isReadable()) {
content.open(QIODevice::ReadOnly);
} else {
content.reset();
}
if (filenameExtension == "tga") {
QImage image = image::readTGA(content);

View file

@ -4,4 +4,6 @@ setup_hifi_library()
link_hifi_libraries(shared shaders task gpu graphics hfm material-networking)
include_hifi_library_headers(networking)
include_hifi_library_headers(image)
include_hifi_library_headers(ktx)
include_hifi_library_headers(ktx)
target_draco()

View file

@ -11,15 +11,15 @@
#include "Baker.h"
#include <shared/HifiTypes.h>
#include "BakerTypes.h"
#include "ModelMath.h"
#include "BuildGraphicsMeshTask.h"
#include "CalculateMeshNormalsTask.h"
#include "CalculateMeshTangentsTask.h"
#include "CalculateBlendshapeNormalsTask.h"
#include "CalculateBlendshapeTangentsTask.h"
#include "PrepareJointsTask.h"
#include "BuildDracoMeshTask.h"
#include "ParseFlowDataTask.h"
namespace baker {
@ -60,12 +60,12 @@ namespace baker {
blendshapesPerMeshOut = blendshapesPerMeshIn;
for (int i = 0; i < (int)blendshapesPerMeshOut.size(); i++) {
const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i];
const auto& tangentsPerBlendshape = tangentsPerBlendshapePerMesh[i];
const auto& normalsPerBlendshape = safeGet(normalsPerBlendshapePerMesh, i);
const auto& tangentsPerBlendshape = safeGet(tangentsPerBlendshapePerMesh, i);
auto& blendshapesOut = blendshapesPerMeshOut[i];
for (int j = 0; j < (int)blendshapesOut.size(); j++) {
const auto& normals = normalsPerBlendshape[j];
const auto& tangents = tangentsPerBlendshape[j];
const auto& normals = safeGet(normalsPerBlendshape, j);
const auto& tangents = safeGet(tangentsPerBlendshape, j);
auto& blendshape = blendshapesOut[j];
blendshape.normals = QVector<glm::vec3>::fromStdVector(normals);
blendshape.tangents = QVector<glm::vec3>::fromStdVector(tangents);
@ -91,10 +91,10 @@ namespace baker {
auto meshesOut = meshesIn;
for (int i = 0; i < numMeshes; i++) {
auto& meshOut = meshesOut[i];
meshOut._mesh = graphicsMeshesIn[i];
meshOut.normals = QVector<glm::vec3>::fromStdVector(normalsPerMeshIn[i]);
meshOut.tangents = QVector<glm::vec3>::fromStdVector(tangentsPerMeshIn[i]);
meshOut.blendshapes = QVector<hfm::Blendshape>::fromStdVector(blendshapesPerMeshIn[i]);
meshOut._mesh = safeGet(graphicsMeshesIn, i);
meshOut.normals = QVector<glm::vec3>::fromStdVector(safeGet(normalsPerMeshIn, i));
meshOut.tangents = QVector<glm::vec3>::fromStdVector(safeGet(tangentsPerMeshIn, i));
meshOut.blendshapes = QVector<hfm::Blendshape>::fromStdVector(safeGet(blendshapesPerMeshIn, i));
}
output = meshesOut;
}
@ -119,12 +119,13 @@ namespace baker {
class BakerEngineBuilder {
public:
using Input = VaryingSet2<hfm::Model::Pointer, GeometryMappingPair>;
using Output = VaryingSet2<hfm::Model::Pointer, MaterialMapping>;
using Input = VaryingSet3<hfm::Model::Pointer, hifi::VariantHash, hifi::URL>;
using Output = VaryingSet4<hfm::Model::Pointer, MaterialMapping, std::vector<hifi::ByteArray>, std::vector<std::vector<hifi::ByteArray>>>;
using JobModel = Task::ModelIO<BakerEngineBuilder, Input, Output>;
void build(JobModel& model, const Varying& input, Varying& output) {
const auto& hfmModelIn = input.getN<Input>(0);
const auto& mapping = input.getN<Input>(1);
const auto& materialMappingBaseURL = input.getN<Input>(2);
// Split up the inputs from hfm::Model
const auto modelPartsIn = model.addJob<GetModelPartsTask>("GetModelParts", hfmModelIn);
@ -157,7 +158,18 @@ namespace baker {
const auto jointIndices = jointInfoOut.getN<PrepareJointsTask::Output>(2);
// Parse material mapping
const auto materialMapping = model.addJob<ParseMaterialMappingTask>("ParseMaterialMapping", mapping);
const auto parseMaterialMappingInputs = ParseMaterialMappingTask::Input(mapping, materialMappingBaseURL).asVarying();
const auto materialMapping = model.addJob<ParseMaterialMappingTask>("ParseMaterialMapping", parseMaterialMappingInputs);
// Build Draco meshes
// NOTE: This task is disabled by default and must be enabled through configuration
// TODO: Tangent support (Needs changes to FBXSerializer_Mesh as well)
// NOTE: Due to an unresolved linker error, BuildDracoMeshTask is not functional on Android
// TODO: Figure out why BuildDracoMeshTask.cpp won't link with draco on Android
const auto buildDracoMeshInputs = BuildDracoMeshTask::Input(meshesIn, normalsPerMesh, tangentsPerMesh).asVarying();
const auto buildDracoMeshOutputs = model.addJob<BuildDracoMeshTask>("BuildDracoMesh", buildDracoMeshInputs);
const auto dracoMeshes = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(0);
const auto materialList = buildDracoMeshOutputs.getN<BuildDracoMeshTask::Output>(1);
// Parse flow data
const auto flowData = model.addJob<ParseFlowDataTask>("ParseFlowData", mapping);
@ -170,20 +182,38 @@ namespace baker {
const auto buildModelInputs = BuildModelTask::Input(hfmModelIn, meshesOut, jointsOut, jointRotationOffsets, jointIndices, flowData).asVarying();
const auto hfmModelOut = model.addJob<BuildModelTask>("BuildModel", buildModelInputs);
output = Output(hfmModelOut, materialMapping);
output = Output(hfmModelOut, materialMapping, dracoMeshes, materialList);
}
};
Baker::Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping) :
Baker::Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL) :
_engine(std::make_shared<Engine>(BakerEngineBuilder::JobModel::create("Baker"), std::make_shared<BakeContext>())) {
_engine->feedInput<BakerEngineBuilder::Input>(0, hfmModel);
_engine->feedInput<BakerEngineBuilder::Input>(1, mapping);
_engine->feedInput<BakerEngineBuilder::Input>(2, materialMappingBaseURL);
}
std::shared_ptr<TaskConfig> Baker::getConfiguration() {
return _engine->getConfiguration();
}
void Baker::run() {
_engine->run();
hfmModel = _engine->getOutput().get<BakerEngineBuilder::Output>().get0();
materialMapping = _engine->getOutput().get<BakerEngineBuilder::Output>().get1();
}
hfm::Model::Pointer Baker::getHFMModel() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get0();
}
MaterialMapping Baker::getMaterialMapping() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get1();
}
const std::vector<hifi::ByteArray>& Baker::getDracoMeshes() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get2();
}
std::vector<std::vector<hifi::ByteArray>> Baker::getDracoMaterialLists() const {
return _engine->getOutput().get<BakerEngineBuilder::Output>().get3();
}
};

View file

@ -12,8 +12,7 @@
#ifndef hifi_baker_Baker_h
#define hifi_baker_Baker_h
#include <QMap>
#include <shared/HifiTypes.h>
#include <hfm/HFM.h>
#include "Engine.h"
@ -24,18 +23,22 @@
namespace baker {
class Baker {
public:
Baker(const hfm::Model::Pointer& hfmModel, const GeometryMappingPair& mapping);
Baker(const hfm::Model::Pointer& hfmModel, const hifi::VariantHash& mapping, const hifi::URL& materialMappingBaseURL);
std::shared_ptr<TaskConfig> getConfiguration();
void run();
// Outputs, available after run() is called
hfm::Model::Pointer hfmModel;
MaterialMapping materialMapping;
hfm::Model::Pointer getHFMModel() const;
MaterialMapping getMaterialMapping() const;
const std::vector<hifi::ByteArray>& getDracoMeshes() const;
// This is a ByteArray and not a std::string because the character sequence can contain the null character (particularly for FBX materials)
std::vector<std::vector<hifi::ByteArray>> getDracoMaterialLists() const;
protected:
EnginePointer _engine;
};
};
#endif //hifi_baker_Baker_h

View file

@ -36,7 +36,6 @@ namespace baker {
using TangentsPerBlendshape = std::vector<std::vector<glm::vec3>>;
using MeshIndicesToModelNames = QHash<int, QString>;
using GeometryMappingPair = std::pair<QUrl, QVariantHash>;
};
#endif // hifi_BakerTypes_h

View file

@ -0,0 +1,251 @@
//
// BuildDracoMeshTask.cpp
// model-baker/src/model-baker
//
// Created by Sabrina Shanman on 2019/02/20.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "BuildDracoMeshTask.h"
// Fix build warnings due to draco headers not casting size_t
#ifdef _WIN32
#pragma warning( push )
#pragma warning( disable : 4267 )
#endif
// gcc and clang
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-compare"
#endif
#ifndef Q_OS_ANDROID
#include <draco/compression/encode.h>
#include <draco/mesh/triangle_soup_mesh_builder.h>
#endif
#ifdef _WIN32
#pragma warning( pop )
#endif
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#include "ModelBakerLogging.h"
#include "ModelMath.h"
#ifndef Q_OS_ANDROID
std::vector<hifi::ByteArray> createMaterialList(const hfm::Mesh& mesh) {
std::vector<hifi::ByteArray> materialList;
for (const auto& meshPart : mesh.parts) {
auto materialID = QVariant(meshPart.materialID).toByteArray();
const auto materialIt = std::find(materialList.cbegin(), materialList.cend(), materialID);
if (materialIt == materialList.cend()) {
materialList.push_back(materialID);
}
}
return materialList;
}
std::unique_ptr<draco::Mesh> createDracoMesh(const hfm::Mesh& mesh, const std::vector<glm::vec3>& normals, const std::vector<glm::vec3>& tangents, const std::vector<hifi::ByteArray>& materialList) {
Q_ASSERT(normals.size() == 0 || normals.size() == mesh.vertices.size());
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
int64_t numTriangles{ 0 };
for (auto& part : mesh.parts) {
int extraQuadTriangleIndices = part.quadTrianglesIndices.size() % 3;
int extraTriangleIndices = part.triangleIndices.size() % 3;
if (extraQuadTriangleIndices != 0 || extraTriangleIndices != 0) {
qCWarning(model_baker) << "Found a mesh part with indices not divisible by three. Some indices will be discarded during Draco mesh creation.";
}
numTriangles += (part.quadTrianglesIndices.size() - extraQuadTriangleIndices) / 3;
numTriangles += (part.triangleIndices.size() - extraTriangleIndices) / 3;
}
if (numTriangles == 0) {
return std::unique_ptr<draco::Mesh>();
}
draco::TriangleSoupMeshBuilder meshBuilder;
meshBuilder.Start(numTriangles);
bool hasNormals{ normals.size() > 0 };
bool hasColors{ mesh.colors.size() > 0 };
bool hasTexCoords{ mesh.texCoords.size() > 0 };
bool hasTexCoords1{ mesh.texCoords1.size() > 0 };
bool hasPerFaceMaterials{ mesh.parts.size() > 1 };
bool needsOriginalIndices{ (!mesh.clusterIndices.empty() || !mesh.blendshapes.empty()) && mesh.originalIndices.size() > 0 };
int normalsAttributeID { -1 };
int colorsAttributeID { -1 };
int texCoordsAttributeID { -1 };
int texCoords1AttributeID { -1 };
int faceMaterialAttributeID { -1 };
int originalIndexAttributeID { -1 };
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
3, draco::DT_FLOAT32);
if (needsOriginalIndices) {
originalIndexAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
1, draco::DT_INT32);
}
if (hasNormals) {
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
3, draco::DT_FLOAT32);
}
if (hasColors) {
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
3, draco::DT_FLOAT32);
}
if (hasTexCoords) {
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
2, draco::DT_FLOAT32);
}
if (hasTexCoords1) {
texCoords1AttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
2, draco::DT_FLOAT32);
}
if (hasPerFaceMaterials) {
faceMaterialAttributeID = meshBuilder.AddAttribute(
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
1, draco::DT_UINT16);
}
auto partIndex = 0;
draco::FaceIndex face;
uint16_t materialID;
for (auto& part : mesh.parts) {
auto materialIt = std::find(materialList.cbegin(), materialList.cend(), QVariant(part.materialID).toByteArray());
materialID = (uint16_t)(materialIt - materialList.cbegin());
auto addFace = [&](const QVector<int>& indices, int index, draco::FaceIndex face) {
int32_t idx0 = indices[index];
int32_t idx1 = indices[index + 1];
int32_t idx2 = indices[index + 2];
if (hasPerFaceMaterials) {
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
}
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
&mesh.vertices[idx0], &mesh.vertices[idx1],
&mesh.vertices[idx2]);
if (needsOriginalIndices) {
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
&mesh.originalIndices[idx0],
&mesh.originalIndices[idx1],
&mesh.originalIndices[idx2]);
}
if (hasNormals) {
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
&normals[idx0], &normals[idx1],
&normals[idx2]);
}
if (hasColors) {
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
&mesh.colors[idx0], &mesh.colors[idx1],
&mesh.colors[idx2]);
}
if (hasTexCoords) {
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
&mesh.texCoords[idx2]);
}
if (hasTexCoords1) {
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
&mesh.texCoords1[idx2]);
}
};
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
addFace(part.quadTrianglesIndices, i, face++);
}
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
addFace(part.triangleIndices, i, face++);
}
partIndex++;
}
auto dracoMesh = meshBuilder.Finalize();
if (!dracoMesh) {
qCWarning(model_baker) << "Failed to finalize the baking of a draco Geometry node";
return std::unique_ptr<draco::Mesh>();
}
// we need to modify unique attribute IDs for custom attributes
// so the attributes are easily retrievable on the other side
if (hasPerFaceMaterials) {
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
}
if (hasTexCoords1) {
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
}
if (needsOriginalIndices) {
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
}
return dracoMesh;
}
#endif // not Q_OS_ANDROID
void BuildDracoMeshTask::configure(const Config& config) {
_encodeSpeed = config.encodeSpeed;
_decodeSpeed = config.decodeSpeed;
}
void BuildDracoMeshTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
#ifdef Q_OS_ANDROID
qCWarning(model_baker) << "BuildDracoMesh is disabled on Android. Output meshes will be empty.";
#else
const auto& meshes = input.get0();
const auto& normalsPerMesh = input.get1();
const auto& tangentsPerMesh = input.get2();
auto& dracoBytesPerMesh = output.edit0();
auto& materialLists = output.edit1();
dracoBytesPerMesh.reserve(meshes.size());
materialLists.reserve(meshes.size());
for (size_t i = 0; i < meshes.size(); i++) {
const auto& mesh = meshes[i];
const auto& normals = baker::safeGet(normalsPerMesh, i);
const auto& tangents = baker::safeGet(tangentsPerMesh, i);
dracoBytesPerMesh.emplace_back();
auto& dracoBytes = dracoBytesPerMesh.back();
materialLists.push_back(createMaterialList(mesh));
const auto& materialList = materialLists.back();
auto dracoMesh = createDracoMesh(mesh, normals, tangents, materialList);
if (dracoMesh) {
draco::Encoder encoder;
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
encoder.SetSpeedOptions(_encodeSpeed, _decodeSpeed);
draco::EncoderBuffer buffer;
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
dracoBytes = hifi::ByteArray(buffer.data(), (int)buffer.size());
}
}
#endif // not Q_OS_ANDROID
}

View file

@ -0,0 +1,48 @@
//
// BuildDracoMeshTask.h
// model-baker/src/model-baker
//
// Created by Sabrina Shanman on 2019/02/20.
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_BuildDracoMeshTask_h
#define hifi_BuildDracoMeshTask_h
#include <hfm/HFM.h>
#include <shared/HifiTypes.h>
#include "Engine.h"
#include "BakerTypes.h"
// BuildDracoMeshTask is disabled by default
class BuildDracoMeshConfig : public baker::JobConfig {
Q_OBJECT
Q_PROPERTY(int encodeSpeed MEMBER encodeSpeed)
Q_PROPERTY(int decodeSpeed MEMBER decodeSpeed)
public:
BuildDracoMeshConfig() : baker::JobConfig(false) {}
int encodeSpeed { 0 };
int decodeSpeed { 5 };
};
class BuildDracoMeshTask {
public:
using Config = BuildDracoMeshConfig;
using Input = baker::VaryingSet3<std::vector<hfm::Mesh>, baker::NormalsPerMesh, baker::TangentsPerMesh>;
using Output = baker::VaryingSet2<std::vector<hifi::ByteArray>, std::vector<std::vector<hifi::ByteArray>>>;
using JobModel = baker::Job::ModelIO<BuildDracoMeshTask, Input, Output, Config>;
void configure(const Config& config);
void run(const baker::BakeContextPointer& context, const Input& input, Output& output);
protected:
int _encodeSpeed { 0 };
int _decodeSpeed { 5 };
};
#endif // hifi_BuildDracoMeshTask_h

View file

@ -15,6 +15,7 @@
#include <LogHandler.h>
#include "ModelBakerLogging.h"
#include "ModelMath.h"
using vec2h = glm::tvec2<glm::detail::hdata>;
@ -385,7 +386,7 @@ void BuildGraphicsMeshTask::run(const baker::BakeContextPointer& context, const
auto& graphicsMesh = graphicsMeshes[i];
// Try to create the graphics::Mesh
buildGraphicsMesh(meshes[i], graphicsMesh, normalsPerMesh[i], tangentsPerMesh[i]);
buildGraphicsMesh(meshes[i], graphicsMesh, baker::safeGet(normalsPerMesh, i), baker::safeGet(tangentsPerMesh, i));
// Choose a name for the mesh
if (graphicsMesh) {

View file

@ -24,7 +24,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte
tangentsPerBlendshapePerMeshOut.reserve(normalsPerBlendshapePerMesh.size());
for (size_t i = 0; i < blendshapesPerMesh.size(); i++) {
const auto& normalsPerBlendshape = normalsPerBlendshapePerMesh[i];
const auto& normalsPerBlendshape = baker::safeGet(normalsPerBlendshapePerMesh, i);
const auto& blendshapes = blendshapesPerMesh[i];
const auto& mesh = meshes[i];
tangentsPerBlendshapePerMeshOut.emplace_back();
@ -43,7 +43,7 @@ void CalculateBlendshapeTangentsTask::run(const baker::BakeContextPointer& conte
for (size_t j = 0; j < blendshapes.size(); j++) {
const auto& blendshape = blendshapes[j];
const auto& tangentsIn = blendshape.tangents;
const auto& normals = normalsPerBlendshape[j];
const auto& normals = baker::safeGet(normalsPerBlendshape, j);
tangentsPerBlendshapeOut.emplace_back();
auto& tangentsOut = tangentsPerBlendshapeOut[tangentsPerBlendshapeOut.size()-1];

View file

@ -34,7 +34,7 @@ void CalculateMeshTangentsTask::run(const baker::BakeContextPointer& context, co
for (int i = 0; i < (int)meshes.size(); i++) {
const auto& mesh = meshes[i];
const auto& tangentsIn = mesh.tangents;
const auto& normals = normalsPerMesh[i];
const auto& normals = baker::safeGet(normalsPerMesh, i);
tangentsPerMeshOut.emplace_back();
auto& tangentsOut = tangentsPerMeshOut[tangentsPerMeshOut.size()-1];

View file

@ -14,6 +14,17 @@
#include "BakerTypes.h"
namespace baker {
template<typename T>
const T& safeGet(const std::vector<T>& data, size_t i) {
static T t;
if (data.size() > i) {
return data[i];
} else {
return t;
}
}
// Returns a reference to the normal at the specified index, or nullptr if it cannot be accessed
using NormalAccessor = std::function<glm::vec3*(int index)>;

View file

@ -8,12 +8,11 @@
#include "ParseFlowDataTask.h"
void ParseFlowDataTask::run(const baker::BakeContextPointer& context, const Input& mappingPair, Output& output) {
void ParseFlowDataTask::run(const baker::BakeContextPointer& context, const Input& mapping, Output& output) {
FlowData flowData;
static const QString FLOW_PHYSICS_FIELD = "flowPhysicsData";
static const QString FLOW_COLLISIONS_FIELD = "flowCollisionsData";
auto mapping = mappingPair.second;
for (auto mappingIter = mapping.begin(); mappingIter != mapping.end(); mappingIter++) {
for (auto mappingIter = mapping.cbegin(); mappingIter != mapping.cend(); mappingIter++) {
if (mappingIter.key() == FLOW_PHYSICS_FIELD || mappingIter.key() == FLOW_COLLISIONS_FIELD) {
QByteArray data = mappingIter.value().toByteArray();
QJsonObject dataObject = QJsonDocument::fromJson(data).object();

View file

@ -12,11 +12,13 @@
#include <hfm/HFM.h>
#include "Engine.h"
#include <shared/HifiTypes.h>
#include "BakerTypes.h"
class ParseFlowDataTask {
public:
using Input = baker::GeometryMappingPair;
using Input = hifi::VariantHash;
using Output = FlowData;
using JobModel = baker::Job::ModelIO<ParseFlowDataTask, Input, Output>;

View file

@ -11,8 +11,8 @@
#include "ModelBakerLogging.h"
void ParseMaterialMappingTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
const auto& url = input.first;
const auto& mapping = input.second;
const auto& mapping = input.get0();
const auto& url = input.get1();
MaterialMapping materialMapping;
auto mappingIter = mapping.find("materialMap");

View file

@ -13,6 +13,8 @@
#include <hfm/HFM.h>
#include <shared/HifiTypes.h>
#include "Engine.h"
#include "BakerTypes.h"
@ -20,7 +22,7 @@
class ParseMaterialMappingTask {
public:
using Input = baker::GeometryMappingPair;
using Input = baker::VaryingSet2<hifi::VariantHash, hifi::URL>;
using Output = MaterialMapping;
using JobModel = baker::Job::ModelIO<ParseMaterialMappingTask, Input, Output>;

View file

@ -13,7 +13,7 @@
#include "ModelBakerLogging.h"
QMap<QString, QString> getJointNameMapping(const QVariantHash& mapping) {
QMap<QString, QString> getJointNameMapping(const hifi::VariantHash& mapping) {
static const QString JOINT_NAME_MAPPING_FIELD = "jointMap";
QMap<QString, QString> hfmToHifiJointNameMap;
if (!mapping.isEmpty() && mapping.contains(JOINT_NAME_MAPPING_FIELD) && mapping[JOINT_NAME_MAPPING_FIELD].type() == QVariant::Hash) {
@ -26,7 +26,7 @@ QMap<QString, QString> getJointNameMapping(const QVariantHash& mapping) {
return hfmToHifiJointNameMap;
}
QMap<QString, glm::quat> getJointRotationOffsets(const QVariantHash& mapping) {
QMap<QString, glm::quat> getJointRotationOffsets(const hifi::VariantHash& mapping) {
QMap<QString, glm::quat> jointRotationOffsets;
static const QString JOINT_ROTATION_OFFSET_FIELD = "jointRotationOffset";
static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2";
@ -56,69 +56,76 @@ QMap<QString, glm::quat> getJointRotationOffsets(const QVariantHash& mapping) {
return jointRotationOffsets;
}
void PrepareJointsTask::configure(const Config& config) {
_passthrough = config.passthrough;
}
void PrepareJointsTask::run(const baker::BakeContextPointer& context, const Input& input, Output& output) {
const auto& jointsIn = input.get0();
const auto& mapping = input.get1();
auto& jointsOut = output.edit0();
auto& jointRotationOffsets = output.edit1();
auto& jointIndices = output.edit2();
bool newJointRot = false;
static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2";
QVariantHash fstHashMap = mapping.second;
if (fstHashMap.contains(JOINT_ROTATION_OFFSET2_FIELD)) {
newJointRot = true;
if (_passthrough) {
jointsOut = jointsIn;
} else {
newJointRot = false;
}
const auto& mapping = input.get1();
auto& jointRotationOffsets = output.edit1();
auto& jointIndices = output.edit2();
// Get joint renames
auto jointNameMapping = getJointNameMapping(mapping.second);
// Apply joint metadata from FST file mappings
for (const auto& jointIn : jointsIn) {
jointsOut.push_back(jointIn);
auto& jointOut = jointsOut.back();
bool newJointRot = false;
static const QString JOINT_ROTATION_OFFSET2_FIELD = "jointRotationOffset2";
QVariantHash fstHashMap = mapping;
if (fstHashMap.contains(JOINT_ROTATION_OFFSET2_FIELD)) {
newJointRot = true;
} else {
newJointRot = false;
}
if (!newJointRot) {
auto jointNameMapKey = jointNameMapping.key(jointIn.name);
if (jointNameMapping.contains(jointNameMapKey)) {
jointOut.name = jointNameMapKey;
// Get joint renames
auto jointNameMapping = getJointNameMapping(mapping);
// Apply joint metadata from FST file mappings
for (const auto& jointIn : jointsIn) {
jointsOut.push_back(jointIn);
auto& jointOut = jointsOut.back();
if (!newJointRot) {
auto jointNameMapKey = jointNameMapping.key(jointIn.name);
if (jointNameMapping.contains(jointNameMapKey)) {
jointOut.name = jointNameMapKey;
}
}
jointIndices.insert(jointOut.name, (int)jointsOut.size());
}
// Get joint rotation offsets from FST file mappings
auto offsets = getJointRotationOffsets(mapping);
for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
QString jointName = itr.key();
int jointIndex = jointIndices.value(jointName) - 1;
if (jointIndex >= 0) {
glm::quat rotationOffset = itr.value();
jointRotationOffsets.insert(jointIndex, rotationOffset);
qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset;
}
}
jointIndices.insert(jointOut.name, (int)jointsOut.size());
}
// Get joint rotation offsets from FST file mappings
auto offsets = getJointRotationOffsets(mapping.second);
for (auto itr = offsets.begin(); itr != offsets.end(); itr++) {
QString jointName = itr.key();
int jointIndex = jointIndices.value(jointName) - 1;
if (jointIndex >= 0) {
glm::quat rotationOffset = itr.value();
jointRotationOffsets.insert(jointIndex, rotationOffset);
qCDebug(model_baker) << "Joint Rotation Offset added to Rig._jointRotationOffsets : " << " jointName: " << jointName << " jointIndex: " << jointIndex << " rotation offset: " << rotationOffset;
}
}
if (newJointRot) {
for (auto& jointOut : jointsOut) {
auto jointNameMapKey = jointNameMapping.key(jointOut.name);
int mappedIndex = jointIndices.value(jointOut.name);
if (jointNameMapping.contains(jointNameMapKey)) {
// delete and replace with hifi name
jointIndices.remove(jointOut.name);
jointOut.name = jointNameMapKey;
jointIndices.insert(jointOut.name, mappedIndex);
} else {
// nothing mapped to this fbx joint name
if (jointNameMapping.contains(jointOut.name)) {
// but the name is in the list of hifi names is mapped to a different joint
int extraIndex = jointIndices.value(jointOut.name);
if (newJointRot) {
for (auto& jointOut : jointsOut) {
auto jointNameMapKey = jointNameMapping.key(jointOut.name);
int mappedIndex = jointIndices.value(jointOut.name);
if (jointNameMapping.contains(jointNameMapKey)) {
// delete and replace with hifi name
jointIndices.remove(jointOut.name);
jointOut.name = "";
jointIndices.insert(jointOut.name, extraIndex);
jointOut.name = jointNameMapKey;
jointIndices.insert(jointOut.name, mappedIndex);
} else {
// nothing mapped to this fbx joint name
if (jointNameMapping.contains(jointOut.name)) {
// but the name is in the list of hifi names is mapped to a different joint
int extraIndex = jointIndices.value(jointOut.name);
jointIndices.remove(jointOut.name);
jointOut.name = "";
jointIndices.insert(jointOut.name, extraIndex);
}
}
}
}

View file

@ -12,20 +12,32 @@
#ifndef hifi_PrepareJointsTask_h
#define hifi_PrepareJointsTask_h
#include <QHash>
#include <shared/HifiTypes.h>
#include <hfm/HFM.h>
#include "Engine.h"
#include "BakerTypes.h"
// The property "passthrough", when enabled, will let the input joints flow to the output unmodified, unlike the disabled property, which discards the data
class PrepareJointsConfig : public baker::JobConfig {
Q_OBJECT
Q_PROPERTY(bool passthrough MEMBER passthrough)
public:
bool passthrough { false };
};
class PrepareJointsTask {
public:
using Input = baker::VaryingSet2<std::vector<hfm::Joint>, baker::GeometryMappingPair /*mapping*/>;
using Config = PrepareJointsConfig;
using Input = baker::VaryingSet2<std::vector<hfm::Joint>, hifi::VariantHash /*mapping*/>;
using Output = baker::VaryingSet3<std::vector<hfm::Joint>, QMap<int, glm::quat> /*jointRotationOffsets*/, QHash<QString, int> /*jointIndices*/>;
using JobModel = baker::Job::ModelIO<PrepareJointsTask, Input, Output>;
using JobModel = baker::Job::ModelIO<PrepareJointsTask, Input, Output, Config>;
void configure(const Config& config);
void run(const baker::BakeContextPointer& context, const Input& input, Output& output);
protected:
bool _passthrough { false };
};
#endif // hifi_PrepareJointsTask_h

View file

@ -249,6 +249,7 @@ void GeometryReader::run() {
HFMModel::Pointer hfmModel;
QVariantHash serializerMapping = _mapping.second;
serializerMapping["combineParts"] = _combineParts;
serializerMapping["deduplicateIndices"] = true;
if (_url.path().toLower().endsWith(".gz")) {
QByteArray uncompressedData;
@ -339,12 +340,12 @@ void GeometryDefinitionResource::downloadFinished(const QByteArray& data) {
void GeometryDefinitionResource::setGeometryDefinition(HFMModel::Pointer hfmModel, const GeometryMappingPair& mapping) {
// Do processing on the model
baker::Baker modelBaker(hfmModel, mapping);
baker::Baker modelBaker(hfmModel, mapping.second, mapping.first);
modelBaker.run();
// Assume ownership of the processed HFMModel
_hfmModel = modelBaker.hfmModel;
_materialMapping = modelBaker.materialMapping;
_hfmModel = modelBaker.getHFMModel();
_materialMapping = modelBaker.getMaterialMapping();
// Copy materials
QHash<QString, size_t> materialIDAtlas;

View file

@ -2,7 +2,7 @@ set(TARGET_NAME oven)
setup_hifi_project(Widgets Gui Concurrent)
link_hifi_libraries(networking shared image gpu ktx fbx hfm baking graphics)
link_hifi_libraries(shared shaders image gpu ktx fbx hfm baking graphics networking material-networking model-baker task)
setup_memory_debugger()

View file

@ -20,9 +20,10 @@
#include "OvenCLIApplication.h"
#include "ModelBakingLoggingCategory.h"
#include "FBXBaker.h"
#include "baking/BakerLibrary.h"
#include "JSBaker.h"
#include "TextureBaker.h"
#include "MaterialBaker.h"
BakerCLI::BakerCLI(OvenCLIApplication* parent) : QObject(parent) {
@ -37,59 +38,68 @@ void BakerCLI::bakeFile(QUrl inputUrl, const QString& outputPath, const QString&
qDebug() << "Baking file type: " << type;
static const QString MODEL_EXTENSION { "fbx" };
static const QString MODEL_EXTENSION { "model" };
static const QString FBX_EXTENSION { "fbx" }; // legacy
static const QString MATERIAL_EXTENSION { "material" };
static const QString SCRIPT_EXTENSION { "js" };
// check what kind of baker we should be creating
bool isFBX = type == MODEL_EXTENSION;
bool isScript = type == SCRIPT_EXTENSION;
// If the type doesn't match the above, we assume we have a texture, and the type specified is the
// texture usage type (albedo, cubemap, normals, etc.)
auto url = inputUrl.toDisplayString();
auto idx = url.lastIndexOf('.');
auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : "";
bool isSupportedImage = QImageReader::supportedImageFormats().contains(extension.toLatin1());
_outputPath = outputPath;
// create our appropiate baker
if (isFBX) {
_baker = std::unique_ptr<Baker> {
new FBXBaker(inputUrl,
[]() -> QThread* { return Oven::instance().getNextWorkerThread(); },
outputPath)
};
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else if (isScript) {
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);
if (_baker) {
_baker->moveToThread(Oven::instance().getNextWorkerThread());
}
}
} else if (type == SCRIPT_EXTENSION) {
_baker = std::unique_ptr<Baker> { new JSBaker(inputUrl, outputPath) };
_baker->moveToThread(Oven::instance().getNextWorkerThread());
} else if (isSupportedImage) {
static const std::unordered_map<QString, image::TextureUsage::Type> STRING_TO_TEXTURE_USAGE_TYPE_MAP {
{ "default", image::TextureUsage::DEFAULT_TEXTURE },
{ "strict", image::TextureUsage::STRICT_TEXTURE },
{ "albedo", image::TextureUsage::ALBEDO_TEXTURE },
{ "normal", image::TextureUsage::NORMAL_TEXTURE },
{ "bump", image::TextureUsage::BUMP_TEXTURE },
{ "specular", image::TextureUsage::SPECULAR_TEXTURE },
{ "metallic", image::TextureUsage::METALLIC_TEXTURE },
{ "roughness", image::TextureUsage::ROUGHNESS_TEXTURE },
{ "gloss", image::TextureUsage::GLOSS_TEXTURE },
{ "emissive", image::TextureUsage::EMISSIVE_TEXTURE },
{ "cube", image::TextureUsage::CUBE_TEXTURE },
{ "occlusion", image::TextureUsage::OCCLUSION_TEXTURE },
{ "scattering", image::TextureUsage::SCATTERING_TEXTURE },
{ "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE },
};
auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type);
if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) {
qCDebug(model_baking) << "Unknown texture usage type:" << type;
QCoreApplication::exit(OVEN_STATUS_CODE_FAIL);
}
_baker = std::unique_ptr<Baker> { new TextureBaker(inputUrl, it->second, outputPath) };
} else if (type == MATERIAL_EXTENSION) {
_baker = std::unique_ptr<Baker> { new MaterialBaker(inputUrl.toDisplayString(), true, outputPath, QUrl(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
// texture usage type (albedo, cubemap, normals, etc.)
auto url = inputUrl.toDisplayString();
auto idx = url.lastIndexOf('.');
auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : "";
if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) {
static const std::unordered_map<QString, image::TextureUsage::Type> STRING_TO_TEXTURE_USAGE_TYPE_MAP {
{ "default", image::TextureUsage::DEFAULT_TEXTURE },
{ "strict", image::TextureUsage::STRICT_TEXTURE },
{ "albedo", image::TextureUsage::ALBEDO_TEXTURE },
{ "normal", image::TextureUsage::NORMAL_TEXTURE },
{ "bump", image::TextureUsage::BUMP_TEXTURE },
{ "specular", image::TextureUsage::SPECULAR_TEXTURE },
{ "metallic", image::TextureUsage::METALLIC_TEXTURE },
{ "roughness", image::TextureUsage::ROUGHNESS_TEXTURE },
{ "gloss", image::TextureUsage::GLOSS_TEXTURE },
{ "emissive", image::TextureUsage::EMISSIVE_TEXTURE },
{ "cube", image::TextureUsage::CUBE_TEXTURE },
{ "skybox", image::TextureUsage::CUBE_TEXTURE },
{ "occlusion", image::TextureUsage::OCCLUSION_TEXTURE },
{ "scattering", image::TextureUsage::SCATTERING_TEXTURE },
{ "lightmap", image::TextureUsage::LIGHTMAP_TEXTURE },
};
auto it = STRING_TO_TEXTURE_USAGE_TYPE_MAP.find(type);
if (it == STRING_TO_TEXTURE_USAGE_TYPE_MAP.end()) {
qCDebug(model_baking) << "Unknown texture usage type:" << type;
QCoreApplication::exit(OVEN_STATUS_CODE_FAIL);
}
_baker = std::unique_ptr<Baker> { new TextureBaker(inputUrl, it->second, outputPath) };
_baker->moveToThread(Oven::instance().getNextWorkerThread());
}
}
if (!_baker) {
qCDebug(model_baking) << "Failed to determine baker type for file" << inputUrl;
QCoreApplication::exit(OVEN_STATUS_CODE_FAIL);
return;

View file

@ -20,8 +20,7 @@
#include "Gzip.h"
#include "Oven.h"
#include "FBXBaker.h"
#include "OBJBaker.h"
#include "baking/BakerLibrary.h"
DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName,
const QString& baseOutputPath, const QUrl& destinationPath,
@ -146,11 +145,192 @@ void DomainBaker::loadLocalFile() {
}
}
const QString ENTITY_MODEL_URL_KEY = "modelURL";
const QString ENTITY_SKYBOX_KEY = "skybox";
const QString ENTITY_SKYBOX_URL_KEY = "url";
const QString ENTITY_KEYLIGHT_KEY = "keyLight";
const QString ENTITY_KEYLIGHT_AMBIENT_URL_KEY = "ambientURL";
void DomainBaker::addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) {
// grab a QUrl for the model URL
QUrl bakeableModelURL = getBakeableModelURL(url);
if (!bakeableModelURL.isEmpty() && (_shouldRebakeOriginals || !isModelBaked(bakeableModelURL))) {
// 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);
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.
// There is a small chance this could break a server workflow relying on the old behavior.
// Url suffix is still propagated to the baked URL if the input URL is an FST.
// Url suffix has always been stripped from the URL when loading the original model file to be baked.
baker->setOutputURLSuffix(url);
// make sure our handler is called when the baker is done
connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_modelBakers.insert(bakeableModelURL, baker);
haveBaker = true;
// move the baker to the baker thread
// and kickoff the bake
baker->moveToThread(Oven::instance().getNextWorkerThread());
QMetaObject::invokeMethod(baker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
}
if (haveBaker) {
// add this QJsonValueRef to our multi hash so that we can easily re-write
// the model URL to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(bakeableModelURL, { property, jsonRef });
}
}
}
void DomainBaker::addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef) {
QString cleanURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString();
auto idx = cleanURL.lastIndexOf('.');
auto extension = idx >= 0 ? url.mid(idx + 1).toLower() : "";
if (QImageReader::supportedImageFormats().contains(extension.toLatin1())) {
// grab a clean version of the URL without a query or fragment
QUrl textureURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// setup a texture baker for this URL, as long as we aren't baking a texture already
if (!_textureBakers.contains(textureURL)) {
// setup a baker for this texture
QSharedPointer<TextureBaker> textureBaker {
new TextureBaker(textureURL, type, _contentOutputPath),
&TextureBaker::deleteLater
};
// make sure our handler is called when the texture baker is done
connect(textureBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedTextureBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_textureBakers.insert(textureURL, textureBaker);
// move the baker to a worker thread and kickoff the bake
textureBaker->moveToThread(Oven::instance().getNextWorkerThread());
QMetaObject::invokeMethod(textureBaker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that it can re-write the texture URL
// to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(textureURL, { property, jsonRef });
} else {
qDebug() << "Texture extension not supported: " << extension;
}
}
void DomainBaker::addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef) {
// grab a clean version of the URL without a query or fragment
QUrl scriptURL = QUrl(url).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// setup a script baker for this URL, as long as we aren't baking a script already
if (!_scriptBakers.contains(scriptURL)) {
// setup a baker for this script
QSharedPointer<JSBaker> scriptBaker {
new JSBaker(scriptURL, _contentOutputPath),
&JSBaker::deleteLater
};
// make sure our handler is called when the script baker is done
connect(scriptBaker.data(), &JSBaker::finished, this, &DomainBaker::handleFinishedScriptBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_scriptBakers.insert(scriptURL, scriptBaker);
// move the baker to a worker thread and kickoff the bake
scriptBaker->moveToThread(Oven::instance().getNextWorkerThread());
QMetaObject::invokeMethod(scriptBaker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that it can re-write the script URL
// to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(scriptURL, { property, jsonRef });
}
void DomainBaker::addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef) {
// grab a clean version of the URL without a query or fragment
QString materialData;
if (isURL) {
materialData = QUrl(data).adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment).toDisplayString();
} else {
materialData = data;
}
// setup a material baker for this URL, as long as we aren't baking a material already
if (!_materialBakers.contains(materialData)) {
// setup a baker for this material
QSharedPointer<MaterialBaker> materialBaker {
new MaterialBaker(data, isURL, _contentOutputPath, _destinationPath),
&MaterialBaker::deleteLater
};
// make sure our handler is called when the material baker is done
connect(materialBaker.data(), &MaterialBaker::finished, this, &DomainBaker::handleFinishedMaterialBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_materialBakers.insert(materialData, materialBaker);
// move the baker to a worker thread and kickoff the bake
materialBaker->moveToThread(Oven::instance().getNextWorkerThread());
QMetaObject::invokeMethod(materialBaker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that it can re-write the material URL
// to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(materialData, { property, jsonRef });
}
// All the Entity Properties that can be baked
// ***************************************************************************************
const QString TYPE_KEY = "type";
// Models
const QString MODEL_URL_KEY = "modelURL";
const QString COMPOUND_SHAPE_URL_KEY = "compoundShapeURL";
const QString GRAB_KEY = "grab";
const QString EQUIPPABLE_INDICATOR_URL_KEY = "equippableIndicatorURL";
const QString ANIMATION_KEY = "animation";
const QString ANIMATION_URL_KEY = "url";
// Textures
const QString TEXTURES_KEY = "textures";
const QString IMAGE_URL_KEY = "imageURL";
const QString X_TEXTURE_URL_KEY = "xTextureURL";
const QString Y_TEXTURE_URL_KEY = "yTextureURL";
const QString Z_TEXTURE_URL_KEY = "zTextureURL";
const QString AMBIENT_LIGHT_KEY = "ambientLight";
const QString AMBIENT_URL_KEY = "ambientURL";
const QString SKYBOX_KEY = "skybox";
const QString SKYBOX_URL_KEY = "url";
// Scripts
const QString SCRIPT_KEY = "script";
const QString SERVER_SCRIPTS_KEY = "serverScripts";
// Materials
const QString MATERIAL_URL_KEY = "materialURL";
const QString MATERIAL_DATA_KEY = "materialData";
// ***************************************************************************************
void DomainBaker::enumerateEntities() {
qDebug() << "Enumerating" << _entities.size() << "entities from domain";
@ -160,109 +340,80 @@ void DomainBaker::enumerateEntities() {
if (it->isObject()) {
auto entity = it->toObject();
// check if this is an entity with a model URL or is a skybox texture
if (entity.contains(ENTITY_MODEL_URL_KEY)) {
// grab a QUrl for the model URL
QUrl modelURL { entity[ENTITY_MODEL_URL_KEY].toString() };
// check if the file pointed to by this URL is a bakeable model, by comparing extensions
auto modelFileName = modelURL.fileName();
static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" };
static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" };
static const QString BAKED_MODEL_EXTENSION = ".baked.fbx";
bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive);
bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive);
bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive);
bool isBakeable = isBakeableFBX || isBakeableOBJ;
if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) {
if (isBakedModel) {
// grab a URL to the original, that we assume is stored a directory up, in the "original" folder
// with just the fbx extension
qDebug() << "Re-baking original for" << modelURL;
auto originalFileName = modelFileName;
originalFileName.replace(".baked", "");
modelURL = modelURL.resolved("../original/" + originalFileName);
qDebug() << "Original must be present at" << modelURL;
} else {
// grab a clean version of the URL without a query or fragment
modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
}
// setup a ModelBaker for this URL, as long as we don't already have one
if (!_modelBakers.contains(modelURL)) {
auto filename = modelURL.fileName();
auto baseName = filename.left(filename.lastIndexOf('.'));
auto subDirName = "/" + baseName;
int i = 1;
while (QDir(_contentOutputPath + subDirName).exists()) {
subDirName = "/" + baseName + "-" + QString::number(i++);
}
QSharedPointer<ModelBaker> baker;
if (isBakeableFBX) {
baker = {
new FBXBaker(modelURL, []() -> QThread* {
return Oven::instance().getNextWorkerThread();
}, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"),
&FBXBaker::deleteLater
};
} else {
baker = {
new OBJBaker(modelURL, []() -> QThread* {
return Oven::instance().getNextWorkerThread();
}, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"),
&OBJBaker::deleteLater
};
}
// make sure our handler is called when the baker is done
connect(baker.data(), &Baker::finished, this, &DomainBaker::handleFinishedModelBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_modelBakers.insert(modelURL, baker);
// move the baker to the baker thread
// and kickoff the bake
baker->moveToThread(Oven::instance().getNextWorkerThread());
QMetaObject::invokeMethod(baker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that we can easily re-write
// the model URL to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(modelURL, *it);
// Models
if (entity.contains(MODEL_URL_KEY)) {
addModelBaker(MODEL_URL_KEY, entity[MODEL_URL_KEY].toString(), *it);
}
if (entity.contains(COMPOUND_SHAPE_URL_KEY)) {
// TODO: Support collision model baking
// Do not combine mesh parts, otherwise the collision behavior will be different
// combineParts is currently only used by OBJBaker (mesh-combining functionality ought to be moved to the asset engine at some point), and is also used by OBJBaker to determine if the material library should be loaded (should be separate flag)
// TODO: this could be optimized so that we don't do the full baking pass for collision shapes,
// but we have to handle the case where it's also used as a modelURL somewhere
//addModelBaker(COMPOUND_SHAPE_URL_KEY, entity[COMPOUND_SHAPE_URL_KEY].toString(), *it);
}
if (entity.contains(ANIMATION_KEY)) {
auto animationObject = entity[ANIMATION_KEY].toObject();
if (animationObject.contains(ANIMATION_URL_KEY)) {
addModelBaker(ANIMATION_KEY + "." + ANIMATION_URL_KEY, animationObject[ANIMATION_URL_KEY].toString(), *it);
}
} else {
// // We check now to see if we have either a texture for a skybox or a keylight, or both.
// if (entity.contains(ENTITY_SKYBOX_KEY)) {
// auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject();
// if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) {
// // we have a URL to a skybox, grab it
// QUrl skyboxURL { skyboxObject[ENTITY_SKYBOX_URL_KEY].toString() };
//
// // setup a bake of the skybox
// bakeSkybox(skyboxURL, *it);
// }
// }
//
// if (entity.contains(ENTITY_KEYLIGHT_KEY)) {
// auto keyLightObject = entity[ENTITY_KEYLIGHT_KEY].toObject();
// if (keyLightObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) {
// // we have a URL to a skybox, grab it
// QUrl skyboxURL { keyLightObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY].toString() };
//
// // setup a bake of the skybox
// bakeSkybox(skyboxURL, *it);
// }
// }
}
if (entity.contains(GRAB_KEY)) {
auto grabObject = entity[GRAB_KEY].toObject();
if (grabObject.contains(EQUIPPABLE_INDICATOR_URL_KEY)) {
addModelBaker(GRAB_KEY + "." + EQUIPPABLE_INDICATOR_URL_KEY, grabObject[EQUIPPABLE_INDICATOR_URL_KEY].toString(), *it);
}
}
// Textures
if (entity.contains(TEXTURES_KEY)) {
if (entity.contains(TYPE_KEY)) {
QString type = entity[TYPE_KEY].toString();
// TODO: handle textures for model entities
if (type == "ParticleEffect" || type == "PolyLine") {
addTextureBaker(TEXTURES_KEY, entity[TEXTURES_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it);
}
}
}
if (entity.contains(IMAGE_URL_KEY)) {
addTextureBaker(IMAGE_URL_KEY, entity[IMAGE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it);
}
if (entity.contains(X_TEXTURE_URL_KEY)) {
addTextureBaker(X_TEXTURE_URL_KEY, entity[X_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it);
}
if (entity.contains(Y_TEXTURE_URL_KEY)) {
addTextureBaker(Y_TEXTURE_URL_KEY, entity[Y_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it);
}
if (entity.contains(Z_TEXTURE_URL_KEY)) {
addTextureBaker(Z_TEXTURE_URL_KEY, entity[Z_TEXTURE_URL_KEY].toString(), image::TextureUsage::DEFAULT_TEXTURE, *it);
}
if (entity.contains(AMBIENT_LIGHT_KEY)) {
auto ambientLight = entity[AMBIENT_LIGHT_KEY].toObject();
if (ambientLight.contains(AMBIENT_URL_KEY)) {
addTextureBaker(AMBIENT_LIGHT_KEY + "." + AMBIENT_URL_KEY, ambientLight[AMBIENT_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it);
}
}
if (entity.contains(SKYBOX_KEY)) {
auto skybox = entity[SKYBOX_KEY].toObject();
if (skybox.contains(SKYBOX_URL_KEY)) {
addTextureBaker(SKYBOX_KEY + "." + SKYBOX_URL_KEY, skybox[SKYBOX_URL_KEY].toString(), image::TextureUsage::CUBE_TEXTURE, *it);
}
}
// Scripts
if (entity.contains(SCRIPT_KEY)) {
addScriptBaker(SCRIPT_KEY, entity[SCRIPT_KEY].toString(), *it);
}
if (entity.contains(SERVER_SCRIPTS_KEY)) {
// TODO: serverScripts can be multiple scripts, need to handle that
}
// Materials
if (entity.contains(MATERIAL_URL_KEY)) {
addMaterialBaker(MATERIAL_URL_KEY, entity[MATERIAL_URL_KEY].toString(), true, *it);
}
if (entity.contains(MATERIAL_DATA_KEY)) {
addMaterialBaker(MATERIAL_DATA_KEY, entity[MATERIAL_DATA_KEY].toString(), false, *it);
}
}
}
@ -271,112 +422,56 @@ void DomainBaker::enumerateEntities() {
emit bakeProgress(0, _totalNumberOfSubBakes);
}
void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) {
auto skyboxFileName = skyboxURL.fileName();
static const QStringList BAKEABLE_SKYBOX_EXTENSIONS {
".jpg", ".png", ".gif", ".bmp", ".pbm", ".pgm", ".ppm", ".xbm", ".xpm", ".svg"
};
auto completeLowerExtension = skyboxFileName.mid(skyboxFileName.indexOf('.')).toLower();
if (BAKEABLE_SKYBOX_EXTENSIONS.contains(completeLowerExtension)) {
// grab a clean version of the URL without a query or fragment
skyboxURL = skyboxURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
// setup a texture baker for this URL, as long as we aren't baking a skybox already
if (!_skyboxBakers.contains(skyboxURL)) {
// setup a baker for this skybox
QSharedPointer<TextureBaker> skyboxBaker {
new TextureBaker(skyboxURL, image::TextureUsage::CUBE_TEXTURE, _contentOutputPath),
&TextureBaker::deleteLater
};
// make sure our handler is called when the skybox baker is done
connect(skyboxBaker.data(), &TextureBaker::finished, this, &DomainBaker::handleFinishedSkyboxBaker);
// insert it into our bakers hash so we hold a strong pointer to it
_skyboxBakers.insert(skyboxURL, skyboxBaker);
// move the baker to a worker thread and kickoff the bake
skyboxBaker->moveToThread(Oven::instance().getNextWorkerThread());
QMetaObject::invokeMethod(skyboxBaker.data(), "bake");
// keep track of the total number of baking entities
++_totalNumberOfSubBakes;
}
// add this QJsonValueRef to our multi hash so that it can re-write the skybox URL
// to the baked version once the baker is complete
_entitiesNeedingRewrite.insert(skyboxURL, entity);
}
}
void DomainBaker::handleFinishedModelBaker() {
auto baker = qobject_cast<ModelBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this FBXBaker is done and everything went according to plan
// this ModelBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getModelURL();
// enumerate the QJsonRef values for the URL of this FBX from our multi hash of
// setup a new URL using the prefix we were passed
auto relativeMappingFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getFullOutputMappingURL().toString());
if (relativeMappingFilePath.startsWith("/")) {
relativeMappingFilePath = relativeMappingFilePath.right(relativeMappingFilePath.length() - 1);
}
QUrl newURL = _destinationPath.resolved(relativeMappingFilePath);
// enumerate the QJsonRef values for the URL of this model from our multi hash of
// entity objects needing a URL re-write
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) {
for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getModelURL())) {
QString property = propertyEntityPair.first;
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = entityValue.toObject();
auto entity = propertyEntityPair.second.toObject();
// grab the old URL
QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() };
if (!property.contains(".")) {
// grab the old URL
QUrl oldURL = entity[property].toString();
// setup a new URL using the prefix we were passed
auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath);
if (relativeFBXFilePath.startsWith("/")) {
relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1);
// set the new URL as the value in our temp QJsonObject
// The fragment, query, and user info from the original model URL should now be present on the filename in the FST file
entity[property] = newURL.toString();
} else {
// Group property
QStringList propertySplit = property.split(".");
assert(propertySplit.length() == 2);
// grab the old URL
auto oldObject = entity[propertySplit[0]].toObject();
QUrl oldURL = oldObject[propertySplit[1]].toString();
// copy the fragment and query, and user info from the old model URL
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
// set the new URL as the value in our temp QJsonObject
oldObject[propertySplit[1]] = newURL.toString();
entity[propertySplit[0]] = oldObject;
}
QUrl newModelURL = _destinationPath.resolved(relativeFBXFilePath);
// copy the fragment and query, and user info from the old model URL
newModelURL.setQuery(oldModelURL.query());
newModelURL.setFragment(oldModelURL.fragment());
newModelURL.setUserInfo(oldModelURL.userInfo());
// set the new model URL as the value in our temp QJsonObject
entity[ENTITY_MODEL_URL_KEY] = newModelURL.toString();
// check if the entity also had an animation at the same URL
// in which case it should be replaced with our baked model URL too
const QString ENTITY_ANIMATION_KEY = "animation";
const QString ENTITIY_ANIMATION_URL_KEY = "url";
if (entity.contains(ENTITY_ANIMATION_KEY)) {
auto animationObject = entity[ENTITY_ANIMATION_KEY].toObject();
if (animationObject.contains(ENTITIY_ANIMATION_URL_KEY)) {
// grab the old animation URL
QUrl oldAnimationURL { animationObject[ENTITIY_ANIMATION_URL_KEY].toString() };
// check if its stripped down version matches our stripped down model URL
if (oldAnimationURL.matches(oldModelURL, QUrl::RemoveQuery | QUrl::RemoveFragment)) {
// the animation URL matched the old model URL, so make the animation URL point to the baked FBX
// with its original query and fragment
auto newAnimationURL = _destinationPath.resolved(relativeFBXFilePath);
newAnimationURL.setQuery(oldAnimationURL.query());
newAnimationURL.setFragment(oldAnimationURL.fragment());
newAnimationURL.setUserInfo(oldAnimationURL.userInfo());
animationObject[ENTITIY_ANIMATION_URL_KEY] = newAnimationURL.toString();
// replace the animation object in the entity object
entity[ENTITY_ANIMATION_KEY] = animationObject;
}
}
}
// replace our temp object with the value referenced by our QJsonValueRef
entityValue = entity;
propertyEntityPair.second = entity;
}
} else {
// this model failed to bake - this doesn't fail the entire bake but we need to add
@ -398,48 +493,63 @@ void DomainBaker::handleFinishedModelBaker() {
}
}
void DomainBaker::handleFinishedSkyboxBaker() {
void DomainBaker::handleFinishedTextureBaker() {
auto baker = qobject_cast<TextureBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this FBXBaker is done and everything went according to plan
// this TextureBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getTextureURL();
// enumerate the QJsonRef values for the URL of this FBX from our multi hash of
// setup a new URL using the prefix we were passed
auto relativeTextureFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getMetaTextureFileName());
if (relativeTextureFilePath.startsWith("/")) {
relativeTextureFilePath = relativeTextureFilePath.right(relativeTextureFilePath.length() - 1);
}
auto newURL = _destinationPath.resolved(relativeTextureFilePath);
// enumerate the QJsonRef values for the URL of this texture from our multi hash of
// entity objects needing a URL re-write
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getTextureURL())) {
for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getTextureURL())) {
QString property = propertyEntityPair.first;
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = entityValue.toObject();
auto entity = propertyEntityPair.second.toObject();
if (entity.contains(ENTITY_SKYBOX_KEY)) {
auto skyboxObject = entity[ENTITY_SKYBOX_KEY].toObject();
if (!property.contains(".")) {
// grab the old URL
QUrl oldURL = entity[property].toString();
if (skyboxObject.contains(ENTITY_SKYBOX_URL_KEY)) {
if (rewriteSkyboxURL(skyboxObject[ENTITY_SKYBOX_URL_KEY], baker)) {
// we re-wrote the URL, replace the skybox object referenced by the entity object
entity[ENTITY_SKYBOX_KEY] = skyboxObject;
}
}
}
// copy the fragment and query, and user info from the old texture URL
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
if (entity.contains(ENTITY_KEYLIGHT_KEY)) {
auto ambientObject = entity[ENTITY_KEYLIGHT_KEY].toObject();
// set the new URL as the value in our temp QJsonObject
entity[property] = newURL.toString();
} else {
// Group property
QStringList propertySplit = property.split(".");
assert(propertySplit.length() == 2);
// grab the old URL
auto oldObject = entity[propertySplit[0]].toObject();
QUrl oldURL = oldObject[propertySplit[1]].toString();
if (ambientObject.contains(ENTITY_KEYLIGHT_AMBIENT_URL_KEY)) {
if (rewriteSkyboxURL(ambientObject[ENTITY_KEYLIGHT_AMBIENT_URL_KEY], baker)) {
// we re-wrote the URL, replace the ambient object referenced by the entity object
entity[ENTITY_KEYLIGHT_KEY] = ambientObject;
}
}
// copy the fragment and query, and user info from the old texture URL
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
// set the new URL as the value in our temp QJsonObject
oldObject[propertySplit[1]] = newURL.toString();
entity[propertySplit[0]] = oldObject;
}
// replace our temp object with the value referenced by our QJsonValueRef
entityValue = entity;
propertyEntityPair.second = entity;
}
} else {
// this skybox failed to bake - this doesn't fail the entire bake but we need to add the errors from
// the model to our warnings
// this texture failed to bake - this doesn't fail the entire bake but we need to add the errors from
// the texture to our warnings
_warningList << baker->getWarnings();
}
@ -447,33 +557,177 @@ void DomainBaker::handleFinishedSkyboxBaker() {
_entitiesNeedingRewrite.remove(baker->getTextureURL());
// drop our shared pointer to this baker so that it gets cleaned up
_skyboxBakers.remove(baker->getTextureURL());
_textureBakers.remove(baker->getTextureURL());
// emit progress to tell listeners how many models we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
// emit progress to tell listeners how many textures we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
// check if this was the last model we needed to re-write and if we are done now
checkIfRewritingComplete();
// check if this was the last texture we needed to re-write and if we are done now
checkIfRewritingComplete();
}
}
bool DomainBaker::rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker) {
// grab the old skybox URL
QUrl oldSkyboxURL { urlValue.toString() };
void DomainBaker::handleFinishedScriptBaker() {
auto baker = qobject_cast<JSBaker*>(sender());
if (oldSkyboxURL.matches(baker->getTextureURL(), QUrl::RemoveQuery | QUrl::RemoveFragment)) {
// change the URL to point to the baked texture with its original query and fragment
if (baker) {
if (!baker->hasErrors()) {
// this JSBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getJSPath();
auto newSkyboxURL = _destinationPath.resolved(baker->getMetaTextureFileName());
newSkyboxURL.setQuery(oldSkyboxURL.query());
newSkyboxURL.setFragment(oldSkyboxURL.fragment());
newSkyboxURL.setUserInfo(oldSkyboxURL.userInfo());
// setup a new URL using the prefix we were passed
auto relativeScriptFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedJSFilePath());
if (relativeScriptFilePath.startsWith("/")) {
relativeScriptFilePath = relativeScriptFilePath.right(relativeScriptFilePath.length() - 1);
}
auto newURL = _destinationPath.resolved(relativeScriptFilePath);
urlValue = newSkyboxURL.toString();
// enumerate the QJsonRef values for the URL of this script from our multi hash of
// entity objects needing a URL re-write
for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getJSPath())) {
QString property = propertyEntityPair.first;
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = propertyEntityPair.second.toObject();
return true;
} else {
return false;
if (!property.contains(".")) {
// grab the old URL
QUrl oldURL = entity[property].toString();
// copy the fragment and query, and user info from the old script URL
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
// set the new URL as the value in our temp QJsonObject
entity[property] = newURL.toString();
} else {
// Group property
QStringList propertySplit = property.split(".");
assert(propertySplit.length() == 2);
// grab the old URL
auto oldObject = entity[propertySplit[0]].toObject();
QUrl oldURL = oldObject[propertySplit[1]].toString();
// copy the fragment and query, and user info from the old script URL
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
// set the new URL as the value in our temp QJsonObject
oldObject[propertySplit[1]] = newURL.toString();
entity[propertySplit[0]] = oldObject;
}
// replace our temp object with the value referenced by our QJsonValueRef
propertyEntityPair.second = entity;
}
} else {
// this script failed to bake - this doesn't fail the entire bake but we need to add
// the errors from the script to our warnings
_warningList << baker->getErrors();
}
// remove the baked URL from the multi hash of entities needing a re-write
_entitiesNeedingRewrite.remove(baker->getJSPath());
// drop our shared pointer to this baker so that it gets cleaned up
_scriptBakers.remove(baker->getJSPath());
// emit progress to tell listeners how many scripts we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
// check if this was the last script we needed to re-write and if we are done now
checkIfRewritingComplete();
}
}
void DomainBaker::handleFinishedMaterialBaker() {
auto baker = qobject_cast<MaterialBaker*>(sender());
if (baker) {
if (!baker->hasErrors()) {
// this MaterialBaker is done and everything went according to plan
qDebug() << "Re-writing entity references to" << baker->getMaterialData();
QString newDataOrURL;
if (baker->isURL()) {
// setup a new URL using the prefix we were passed
auto relativeMaterialFilePath = QDir(_contentOutputPath).relativeFilePath(baker->getBakedMaterialData());
if (relativeMaterialFilePath.startsWith("/")) {
relativeMaterialFilePath = relativeMaterialFilePath.right(relativeMaterialFilePath.length() - 1);
}
newDataOrURL = _destinationPath.resolved(relativeMaterialFilePath).toDisplayString();
} else {
newDataOrURL = baker->getBakedMaterialData();
}
// enumerate the QJsonRef values for the URL of this material from our multi hash of
// entity objects needing a URL re-write
for (auto propertyEntityPair : _entitiesNeedingRewrite.values(baker->getMaterialData())) {
QString property = propertyEntityPair.first;
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
auto entity = propertyEntityPair.second.toObject();
if (!property.contains(".")) {
// grab the old URL
QUrl oldURL = entity[property].toString();
// copy the fragment and query, and user info from the old material data
if (baker->isURL()) {
QUrl newURL = newDataOrURL;
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
// set the new URL as the value in our temp QJsonObject
entity[property] = newURL.toString();
} else {
entity[property] = newDataOrURL;
}
} else {
// Group property
QStringList propertySplit = property.split(".");
assert(propertySplit.length() == 2);
// grab the old URL
auto oldObject = entity[propertySplit[0]].toObject();
QUrl oldURL = oldObject[propertySplit[1]].toString();
// copy the fragment and query, and user info from the old material data
if (baker->isURL()) {
QUrl newURL = newDataOrURL;
newURL.setQuery(oldURL.query());
newURL.setFragment(oldURL.fragment());
newURL.setUserInfo(oldURL.userInfo());
// set the new URL as the value in our temp QJsonObject
oldObject[propertySplit[1]] = newURL.toString();
entity[propertySplit[0]] = oldObject;
} else {
oldObject[propertySplit[1]] = newDataOrURL;
entity[propertySplit[0]] = oldObject;
}
}
// replace our temp object with the value referenced by our QJsonValueRef
propertyEntityPair.second = entity;
}
} 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->getErrors();
}
// remove the baked URL from the multi hash of entities needing a re-write
_entitiesNeedingRewrite.remove(baker->getMaterialData());
// drop our shared pointer to this baker so that it gets cleaned up
_materialBakers.remove(baker->getMaterialData());
// emit progress to tell listeners how many materials we have baked
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
// check if this was the last material we needed to re-write and if we are done now
checkIfRewritingComplete();
}
}
@ -524,6 +778,5 @@ void DomainBaker::writeNewEntitiesFile() {
return;
}
qDebug() << "Exported entities file with baked model URLs to" << bakedEntitiesFilePath;
qDebug() << "Exported baked entities file to" << bakedEntitiesFilePath;
}

View file

@ -17,9 +17,10 @@
#include <QtCore/QUrl>
#include <QtCore/QThread>
#include "Baker.h"
#include "FBXBaker.h"
#include "ModelBaker.h"
#include "TextureBaker.h"
#include "JSBaker.h"
#include "MaterialBaker.h"
class DomainBaker : public Baker {
Q_OBJECT
@ -29,7 +30,7 @@ public:
// That means you must pass a usable running QThread when constructing a domain baker.
DomainBaker(const QUrl& localEntitiesFileURL, const QString& domainName,
const QString& baseOutputPath, const QUrl& destinationPath,
bool shouldRebakeOriginals = false);
bool shouldRebakeOriginals);
signals:
void allModelsFinished();
@ -38,7 +39,9 @@ signals:
private slots:
virtual void bake() override;
void handleFinishedModelBaker();
void handleFinishedSkyboxBaker();
void handleFinishedTextureBaker();
void handleFinishedScriptBaker();
void handleFinishedMaterialBaker();
private:
void setupOutputFolder();
@ -47,9 +50,6 @@ private:
void checkIfRewritingComplete();
void writeNewEntitiesFile();
void bakeSkybox(QUrl skyboxURL, QJsonValueRef entity);
bool rewriteSkyboxURL(QJsonValueRef urlValue, TextureBaker* baker);
QUrl _localEntitiesFileURL;
QString _domainName;
QString _baseOutputPath;
@ -62,14 +62,21 @@ private:
QJsonArray _entities;
QHash<QUrl, QSharedPointer<ModelBaker>> _modelBakers;
QHash<QUrl, QSharedPointer<TextureBaker>> _skyboxBakers;
QHash<QUrl, QSharedPointer<TextureBaker>> _textureBakers;
QHash<QUrl, QSharedPointer<JSBaker>> _scriptBakers;
QHash<QUrl, QSharedPointer<MaterialBaker>> _materialBakers;
QMultiHash<QUrl, QJsonValueRef> _entitiesNeedingRewrite;
QMultiHash<QUrl, std::pair<QString, QJsonValueRef>> _entitiesNeedingRewrite;
int _totalNumberOfSubBakes { 0 };
int _completedSubBakes { 0 };
bool _shouldRebakeOriginals { false };
void addModelBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef);
void addTextureBaker(const QString& property, const QString& url, image::TextureUsage::Type type, const QJsonValueRef& jsonRef);
void addScriptBaker(const QString& property, const QString& url, const QJsonValueRef& jsonRef);
void addMaterialBaker(const QString& property, const QString& data, bool isURL, const QJsonValueRef& jsonRef);
};
#endif // hifi_DomainBaker_h

View file

@ -20,6 +20,13 @@
#include <StatTracker.h>
#include <ResourceManager.h>
#include <ResourceRequestObserver.h>
#include <ResourceCache.h>
#include <material-networking/TextureCache.h>
#include <hfm/ModelFormatRegistry.h>
#include <FBXSerializer.h>
#include <OBJSerializer.h>
#include "MaterialBaker.h"
Oven* Oven::_staticInstance { nullptr };
@ -33,6 +40,18 @@ Oven::Oven() {
DependencyManager::set<StatTracker>();
DependencyManager::set<ResourceManager>(false);
DependencyManager::set<ResourceRequestObserver>();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<TextureCache>();
MaterialBaker::setNextOvenWorkerThreadOperator([] {
return Oven::instance().getNextWorkerThread();
});
{
auto modelFormatRegistry = DependencyManager::set<ModelFormatRegistry>();
modelFormatRegistry->addFormat(FBXSerializer());
modelFormatRegistry->addFormat(OBJSerializer());
}
}
Oven::~Oven() {
@ -63,6 +82,10 @@ void Oven::setupWorkerThreads(int numWorkerThreads) {
}
QThread* Oven::getNextWorkerThread() {
// FIXME: we assign these threads when we make the bakers, but if certain bakers finish quickly, we could end up
// in a situation where threads have finished and others have tons of work queued. Instead of assigning them at initialization,
// we should build a queue of bakers, and when threads finish, they can take the next available baker.
// Here we replicate some of the functionality of QThreadPool by giving callers an available worker thread to use.
// We can't use QThreadPool because we want to put QObjects with signals/slots on these threads.
// So instead we setup our own list of threads, up to one less than the ideal thread count

View file

@ -33,7 +33,7 @@ OvenCLIApplication::OvenCLIApplication(int argc, char* argv[]) :
parser.addOptions({
{ CLI_INPUT_PARAMETER, "Path to file that you would like to bake.", "input" },
{ CLI_OUTPUT_PARAMETER, "Path to folder that will be used as output.", "output" },
{ CLI_TYPE_PARAMETER, "Type of asset.", "type" },
{ CLI_TYPE_PARAMETER, "Type of asset. [model|material|js]", "type" },
{ CLI_DISABLE_TEXTURE_COMPRESSION_PARAMETER, "Disable texture compression." }
});

View file

@ -26,8 +26,7 @@
#include "../Oven.h"
#include "../OvenGUIApplication.h"
#include "OvenMainWindow.h"
#include "FBXBaker.h"
#include "OBJBaker.h"
#include "baking/BakerLibrary.h"
static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory";
@ -117,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() {
startDir = QDir::homePath();
}
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)");
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj *.gltf *.fst)");
if (!selectedFiles.isEmpty()) {
// set the contents of the model file text box to be the path to the selected file
@ -166,80 +165,47 @@ void ModelBakeWidget::bakeButtonClicked() {
return;
}
// make sure we have a valid output directory
QDir outputDirectory(_outputDirLineEdit->text());
if (!outputDirectory.exists()) {
QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory.");
return;
}
// split the list from the model line edit to see how many models we need to bake
auto fileURLStrings = _modelLineEdit->text().split(',');
foreach (QString fileURLString, fileURLStrings) {
// construct a URL from the path in the model file text box
QUrl modelToBakeURL(fileURLString);
QUrl modelToBakeURL = QUrl::fromUserInput(fileURLString);
// if the URL doesn't have a scheme, assume it is a local file
if (modelToBakeURL.scheme() != "http" && modelToBakeURL.scheme() != "https" && modelToBakeURL.scheme() != "ftp") {
qDebug() << modelToBakeURL.toString();
qDebug() << modelToBakeURL.scheme();
modelToBakeURL = QUrl::fromLocalFile(fileURLString);
qDebug() << "New url: " << modelToBakeURL;
QUrl bakeableModelURL = getBakeableModelURL(modelToBakeURL);
if (!bakeableModelURL.isEmpty()) {
auto getWorkerThreadCallback = []() -> QThread* {
return Oven::instance().getNextWorkerThread();
};
std::unique_ptr<Baker> baker = getModelBaker(bakeableModelURL, getWorkerThreadCallback, outputDirectory.path());
if (baker) {
// everything seems to be in place, kick off a bake for this model now
// move the baker to the FBX baker thread
baker->moveToThread(Oven::instance().getNextWorkerThread());
// invoke the bake method on the baker thread
QMetaObject::invokeMethod(baker.get(), "bake");
// make sure we hear about the results of this baker when it is done
connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker);
// add a pending row to the results window to show that this bake is in process
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory);
// keep a unique_ptr to this baker
// and remember the row that represents it in the results table
_bakers.emplace_back(std::move(baker), resultsRow);
}
}
auto modelName = modelToBakeURL.fileName().left(modelToBakeURL.fileName().lastIndexOf('.'));
// make sure we have a valid output directory
QDir outputDirectory(_outputDirLineEdit->text());
QString subFolderName = modelName + "/";
// output in a sub-folder with the name of the fbx, potentially suffixed by a number to make it unique
int iteration = 0;
while (outputDirectory.exists(subFolderName)) {
subFolderName = modelName + "-" + QString::number(++iteration) + "/";
}
outputDirectory.mkpath(subFolderName);
if (!outputDirectory.exists()) {
QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory.");
return;
}
outputDirectory.cd(subFolderName);
QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked");
QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original");
bakedOutputDirectory.mkdir(".");
originalOutputDirectory.mkdir(".");
std::unique_ptr<Baker> baker;
auto getWorkerThreadCallback = []() -> QThread* {
return Oven::instance().getNextWorkerThread();
};
// everything seems to be in place, kick off a bake for this model now
if (modelToBakeURL.fileName().endsWith(".fbx")) {
baker.reset(new FBXBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(),
originalOutputDirectory.absolutePath()));
} else if (modelToBakeURL.fileName().endsWith(".obj")) {
baker.reset(new OBJBaker(modelToBakeURL, getWorkerThreadCallback, bakedOutputDirectory.absolutePath(),
originalOutputDirectory.absolutePath()));
} else {
qWarning() << "Unknown model type: " << modelToBakeURL.fileName();
continue;
}
// move the baker to the FBX baker thread
baker->moveToThread(Oven::instance().getNextWorkerThread());
// invoke the bake method on the baker thread
QMetaObject::invokeMethod(baker.get(), "bake");
// make sure we hear about the results of this baker when it is done
connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker);
// add a pending row to the results window to show that this bake is in process
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
auto resultsRow = resultsWindow->addPendingResultRow(modelToBakeURL.fileName(), outputDirectory);
// keep a unique_ptr to this baker
// and remember the row that represents it in the results table
_bakers.emplace_back(std::move(baker), resultsRow);
}
}

View file

@ -42,12 +42,14 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, HFMModel& result) {
return false;
}
try {
QByteArray fbxContents = fbx.readAll();
hifi::ByteArray fbxContents = fbx.readAll();
HFMModel::Pointer hfmModel;
hifi::VariantHash mapping;
mapping["deduplicateIndices"] = true;
if (filename.toLower().endsWith(".obj")) {
hfmModel = OBJSerializer().read(fbxContents, QVariantHash(), filename);
hfmModel = OBJSerializer().read(fbxContents, mapping, filename);
} else if (filename.toLower().endsWith(".fbx")) {
hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), filename);
hfmModel = FBXSerializer().read(fbxContents, mapping, filename);
} else {
qWarning() << "file has unknown extension" << filename;
return false;