mirror of
https://github.com/AleziaKurdis/overte.git
synced 2025-04-10 14:53:00 +02:00
Merge branch 'master' of github.com:highfidelity/hifi into commerce_upgrades_1
This commit is contained in:
commit
daa68768b9
28 changed files with 1452 additions and 779 deletions
|
@ -5,8 +5,8 @@
|
|||
|
||||
{ "from": "TouchscreenVirtualPad.LX", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" },
|
||||
|
||||
{ "from": "TouchscreenVirtualPad.RX", "when": "!Application.CameraIndependent", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.Yaw" },
|
||||
{ "from": "TouchscreenVirtualPad.RX", "when": "!Application.CameraIndependent", "filters": [ {"type": "deadZone", "min": 0.05} , "invert" ], "to": "Actions.Yaw" },
|
||||
|
||||
{ "from": "TouchscreenVirtualPad.RY", "when": "!Application.CameraIndependent", "to": "Actions.Pitch" }
|
||||
{ "from": "TouchscreenVirtualPad.RY", "when": "!Application.CameraIndependent", "filters": [ {"type": "deadZone", "min": 0.05}, "invert" ], "to": "Actions.Pitch" }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ ModalWindow {
|
|||
keyboardOverride: true // Disable ModalWindow's keyboard.
|
||||
|
||||
function tryDestroy() {
|
||||
root.destroy()
|
||||
Controller.setVPadHidden(false);
|
||||
root.destroy();
|
||||
}
|
||||
|
||||
LoginDialog {
|
||||
|
@ -52,8 +53,9 @@ ModalWindow {
|
|||
|
||||
Component.onCompleted: {
|
||||
this.anchors.centerIn = undefined;
|
||||
this.y=150;
|
||||
this.x=(parent.width - this.width)/2;
|
||||
this.y = 150;
|
||||
this.x = (parent.width - this.width) / 2;
|
||||
Controller.setVPadHidden(true);
|
||||
}
|
||||
|
||||
Keys.onPressed: {
|
||||
|
|
|
@ -224,7 +224,7 @@ Item {
|
|||
|
||||
onClicked: {
|
||||
Qt.inputMethod.hide();
|
||||
root.destroy();
|
||||
root.tryDestroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,65 +33,8 @@
|
|||
|
||||
#include "FBXBaker.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 4267 )
|
||||
#endif
|
||||
|
||||
#include <draco/mesh/triangle_soup_mesh_builder.h>
|
||||
#include <draco/compression/encode.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( pop )
|
||||
#endif
|
||||
|
||||
|
||||
FBXBaker::FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
||||
const QString& bakedOutputDir, const QString& originalOutputDir) :
|
||||
_fbxURL(fbxURL),
|
||||
_bakedOutputDir(bakedOutputDir),
|
||||
_originalOutputDir(originalOutputDir),
|
||||
_textureThreadGetter(textureThreadGetter)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
FBXBaker::~FBXBaker() {
|
||||
if (_tempDir.exists()) {
|
||||
if (!_tempDir.remove(_originalFBXFilePath)) {
|
||||
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalFBXFilePath;
|
||||
}
|
||||
if (!_tempDir.rmdir(".")) {
|
||||
qCWarning(model_baking) << "Failed to remove temporary directory:" << _tempDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::abort() {
|
||||
Baker::abort();
|
||||
|
||||
// tell our underlying TextureBaker instances to abort
|
||||
// the FBXBaker will wait until all are aborted before emitting its own abort signal
|
||||
for (auto& textureBaker : _bakingTextures) {
|
||||
textureBaker->abort();
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::bake() {
|
||||
qDebug() << "FBXBaker" << _fbxURL << "bake starting";
|
||||
|
||||
auto tempDir = PathUtils::generateTemporaryDir();
|
||||
|
||||
if (tempDir.isEmpty()) {
|
||||
handleError("Failed to create a temporary directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
_tempDir = tempDir;
|
||||
|
||||
_originalFBXFilePath = _tempDir.filePath(_fbxURL.fileName());
|
||||
qDebug() << "Made temporary dir " << _tempDir;
|
||||
qDebug() << "Origin file path: " << _originalFBXFilePath;
|
||||
void FBXBaker::bake() {
|
||||
qDebug() << "FBXBaker" << _modelURL << "bake starting";
|
||||
|
||||
// setup the output folder for the results of this bake
|
||||
setupOutputFolder();
|
||||
|
@ -152,7 +95,7 @@ void FBXBaker::setupOutputFolder() {
|
|||
}
|
||||
// attempt to make the output folder
|
||||
if (!QDir().mkpath(_originalOutputDir)) {
|
||||
handleError("Failed to create FBX output folder " + _bakedOutputDir);
|
||||
handleError("Failed to create FBX output folder " + _originalOutputDir);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -160,25 +103,25 @@ void FBXBaker::setupOutputFolder() {
|
|||
|
||||
void FBXBaker::loadSourceFBX() {
|
||||
// check if the FBX is local or first needs to be downloaded
|
||||
if (_fbxURL.isLocalFile()) {
|
||||
if (_modelURL.isLocalFile()) {
|
||||
// load up the local file
|
||||
QFile localFBX { _fbxURL.toLocalFile() };
|
||||
QFile localFBX { _modelURL.toLocalFile() };
|
||||
|
||||
qDebug() << "Local file url: " << _fbxURL << _fbxURL.toString() << _fbxURL.toLocalFile() << ", copying to: " << _originalFBXFilePath;
|
||||
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
|
||||
|
||||
if (!localFBX.exists()) {
|
||||
//QMessageBox::warning(this, "Could not find " + _fbxURL.toString(), "");
|
||||
handleError("Could not find " + _fbxURL.toString());
|
||||
handleError("Could not find " + _modelURL.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// make a copy in the output folder
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
qDebug() << "Copying to: " << _originalOutputDir << "/" << _fbxURL.fileName();
|
||||
localFBX.copy(_originalOutputDir + "/" + _fbxURL.fileName());
|
||||
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
|
||||
localFBX.copy(_originalOutputDir + "/" + _modelURL.fileName());
|
||||
}
|
||||
|
||||
localFBX.copy(_originalFBXFilePath);
|
||||
localFBX.copy(_originalModelFilePath);
|
||||
|
||||
// emit our signal to start the import of the FBX source copy
|
||||
emit sourceCopyReadyToLoad();
|
||||
|
@ -193,9 +136,9 @@ void FBXBaker::loadSourceFBX() {
|
|||
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
||||
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
|
||||
|
||||
networkRequest.setUrl(_fbxURL);
|
||||
networkRequest.setUrl(_modelURL);
|
||||
|
||||
qCDebug(model_baking) << "Downloading" << _fbxURL;
|
||||
qCDebug(model_baking) << "Downloading" << _modelURL;
|
||||
auto networkReply = networkAccessManager.get(networkRequest);
|
||||
|
||||
connect(networkReply, &QNetworkReply::finished, this, &FBXBaker::handleFBXNetworkReply);
|
||||
|
@ -206,20 +149,20 @@ void FBXBaker::handleFBXNetworkReply() {
|
|||
auto requestReply = qobject_cast<QNetworkReply*>(sender());
|
||||
|
||||
if (requestReply->error() == QNetworkReply::NoError) {
|
||||
qCDebug(model_baking) << "Downloaded" << _fbxURL;
|
||||
qCDebug(model_baking) << "Downloaded" << _modelURL;
|
||||
|
||||
// grab the contents of the reply and make a copy in the output folder
|
||||
QFile copyOfOriginal(_originalFBXFilePath);
|
||||
QFile copyOfOriginal(_originalModelFilePath);
|
||||
|
||||
qDebug(model_baking) << "Writing copy of original FBX to" << _originalFBXFilePath << copyOfOriginal.fileName();
|
||||
qDebug(model_baking) << "Writing copy of original FBX to" << _originalModelFilePath << copyOfOriginal.fileName();
|
||||
|
||||
if (!copyOfOriginal.open(QIODevice::WriteOnly)) {
|
||||
// add an error to the error list for this FBX stating that a duplicate of the original FBX could not be made
|
||||
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to open " + _originalFBXFilePath + ")");
|
||||
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to open " + _originalModelFilePath + ")");
|
||||
return;
|
||||
}
|
||||
if (copyOfOriginal.write(requestReply->readAll()) == -1) {
|
||||
handleError("Could not create copy of " + _fbxURL.toString() + " (Failed to write)");
|
||||
handleError("Could not create copy of " + _modelURL.toString() + " (Failed to write)");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -227,98 +170,32 @@ void FBXBaker::handleFBXNetworkReply() {
|
|||
copyOfOriginal.close();
|
||||
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
copyOfOriginal.copy(_originalOutputDir + "/" + _fbxURL.fileName());
|
||||
copyOfOriginal.copy(_originalOutputDir + "/" + _modelURL.fileName());
|
||||
}
|
||||
|
||||
// emit our signal to start the import of the FBX source copy
|
||||
emit sourceCopyReadyToLoad();
|
||||
} else {
|
||||
// add an error to our list stating that the FBX could not be downloaded
|
||||
handleError("Failed to download " + _fbxURL.toString());
|
||||
handleError("Failed to download " + _modelURL.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::importScene() {
|
||||
qDebug() << "file path: " << _originalFBXFilePath.toLocal8Bit().data() << QDir(_originalFBXFilePath).exists();
|
||||
qDebug() << "file path: " << _originalModelFilePath.toLocal8Bit().data() << QDir(_originalModelFilePath).exists();
|
||||
|
||||
QFile fbxFile(_originalFBXFilePath);
|
||||
QFile fbxFile(_originalModelFilePath);
|
||||
if (!fbxFile.open(QIODevice::ReadOnly)) {
|
||||
handleError("Error opening " + _originalFBXFilePath + " for reading");
|
||||
handleError("Error opening " + _originalModelFilePath + " for reading");
|
||||
return;
|
||||
}
|
||||
|
||||
FBXReader reader;
|
||||
|
||||
qCDebug(model_baking) << "Parsing" << _fbxURL;
|
||||
qCDebug(model_baking) << "Parsing" << _modelURL;
|
||||
_rootNode = reader._rootNode = reader.parseFBX(&fbxFile);
|
||||
_geometry = reader.extractFBXGeometry({}, _fbxURL.toString());
|
||||
_textureContent = reader._textureContent;
|
||||
}
|
||||
|
||||
QString texturePathRelativeToFBX(QUrl fbxURL, QUrl textureURL) {
|
||||
auto fbxPath = fbxURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
|
||||
if (texturePath.startsWith(fbxPath)) {
|
||||
// texture path is a child of the FBX path, return the texture path without the fbx path
|
||||
return texturePath.mid(fbxPath.length());
|
||||
} else {
|
||||
// the texture path was not a child of the FBX path, return the empty string
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
QString FBXBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
|
||||
// first make sure we have a unique base name for this texture
|
||||
// in case another texture referenced by this model has the same base name
|
||||
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
|
||||
|
||||
QString bakedTextureFileName { textureFileInfo.completeBaseName() };
|
||||
|
||||
if (nameMatches > 0) {
|
||||
// there are already nameMatches texture with this name
|
||||
// append - and that number to our baked texture file name so that it is unique
|
||||
bakedTextureFileName += "-" + QString::number(nameMatches);
|
||||
}
|
||||
|
||||
bakedTextureFileName += BAKED_TEXTURE_EXT;
|
||||
|
||||
// increment the number of name matches
|
||||
++nameMatches;
|
||||
|
||||
return bakedTextureFileName;
|
||||
}
|
||||
|
||||
QUrl FBXBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
|
||||
|
||||
QUrl urlToTexture;
|
||||
|
||||
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
|
||||
|
||||
if (isEmbedded) {
|
||||
urlToTexture = _fbxURL.toString() + "/" + apparentRelativePath.filePath();
|
||||
} else {
|
||||
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
|
||||
// set the texture URL to the local texture that we have confirmed exists
|
||||
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
|
||||
} else {
|
||||
// external texture that we'll need to download or find
|
||||
|
||||
// this is a relative file path which will require different handling
|
||||
// depending on the location of the original FBX
|
||||
if (_fbxURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
|
||||
// the absolute path we ran into for the texture in the FBX exists on this machine
|
||||
// so use that file
|
||||
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
|
||||
} else {
|
||||
// we didn't find the texture on this machine at the absolute path
|
||||
// so assume that it is right beside the FBX to match the behaviour of interface
|
||||
urlToTexture = _fbxURL.resolved(apparentRelativePath.fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlToTexture;
|
||||
_geometry = reader.extractFBXGeometry({}, _modelURL.toString());
|
||||
_textureContentMap = reader._textureContent;
|
||||
}
|
||||
|
||||
void FBXBaker::rewriteAndBakeSceneModels() {
|
||||
|
@ -344,176 +221,25 @@ void FBXBaker::rewriteAndBakeSceneModels() {
|
|||
|
||||
// TODO Pull this out of _geometry instead so we don't have to reprocess it
|
||||
auto extractedMesh = FBXReader::extractMesh(objectChild, meshIndex, false);
|
||||
auto& mesh = extractedMesh.mesh;
|
||||
|
||||
if (mesh.wasCompressed) {
|
||||
handleError("Cannot re-bake a file that contains compressed mesh");
|
||||
return;
|
||||
}
|
||||
|
||||
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
|
||||
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
|
||||
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
|
||||
|
||||
int64_t numTriangles { 0 };
|
||||
for (auto& part : mesh.parts) {
|
||||
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
|
||||
handleWarning("Found a mesh part with invalid index data, skipping");
|
||||
|
||||
// Callback to get MaterialID
|
||||
GetMaterialIDCallback materialIDcallback = [&extractedMesh](int partIndex) {
|
||||
return extractedMesh.partMaterialTextures[partIndex].first;
|
||||
};
|
||||
|
||||
// Compress mesh information and store in dracoMeshNode
|
||||
FBXNode dracoMeshNode;
|
||||
bool success = compressMesh(extractedMesh.mesh, hasDeformers, dracoMeshNode, materialIDcallback);
|
||||
|
||||
// if bake fails - return, if there were errors and continue, if there were warnings.
|
||||
if (!success) {
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
} else if (hasWarnings()) {
|
||||
continue;
|
||||
}
|
||||
numTriangles += part.quadTrianglesIndices.size() / 3;
|
||||
numTriangles += part.triangleIndices.size() / 3;
|
||||
}
|
||||
|
||||
if (numTriangles == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
draco::TriangleSoupMeshBuilder meshBuilder;
|
||||
|
||||
meshBuilder.Start(numTriangles);
|
||||
|
||||
bool hasNormals { mesh.normals.size() > 0 };
|
||||
bool hasColors { mesh.colors.size() > 0 };
|
||||
bool hasTexCoords { mesh.texCoords.size() > 0 };
|
||||
bool hasTexCoords1 { mesh.texCoords1.size() > 0 };
|
||||
bool hasPerFaceMaterials { mesh.parts.size() > 1
|
||||
|| extractedMesh.partMaterialTextures[0].first != 0 };
|
||||
bool needsOriginalIndices { hasDeformers };
|
||||
|
||||
int normalsAttributeID { -1 };
|
||||
int colorsAttributeID { -1 };
|
||||
int texCoordsAttributeID { -1 };
|
||||
int texCoords1AttributeID { -1 };
|
||||
int faceMaterialAttributeID { -1 };
|
||||
int originalIndexAttributeID { -1 };
|
||||
|
||||
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
|
||||
3, draco::DT_FLOAT32);
|
||||
if (needsOriginalIndices) {
|
||||
originalIndexAttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
|
||||
1, draco::DT_INT32);
|
||||
}
|
||||
|
||||
if (hasNormals) {
|
||||
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
|
||||
3, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasColors) {
|
||||
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
|
||||
3, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasTexCoords) {
|
||||
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
|
||||
2, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasTexCoords1) {
|
||||
texCoords1AttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
|
||||
2, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasPerFaceMaterials) {
|
||||
faceMaterialAttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
|
||||
1, draco::DT_UINT16);
|
||||
}
|
||||
|
||||
|
||||
auto partIndex = 0;
|
||||
draco::FaceIndex face;
|
||||
for (auto& part : mesh.parts) {
|
||||
const auto& matTex = extractedMesh.partMaterialTextures[partIndex];
|
||||
uint16_t materialID = matTex.first;
|
||||
|
||||
auto addFace = [&](QVector<int>& indices, int index, draco::FaceIndex face) {
|
||||
int32_t idx0 = indices[index];
|
||||
int32_t idx1 = indices[index + 1];
|
||||
int32_t idx2 = indices[index + 2];
|
||||
|
||||
if (hasPerFaceMaterials) {
|
||||
meshBuilder.SetPerFaceAttributeValueForFace(faceMaterialAttributeID, face, &materialID);
|
||||
}
|
||||
|
||||
meshBuilder.SetAttributeValuesForFace(positionAttributeID, face,
|
||||
&mesh.vertices[idx0], &mesh.vertices[idx1],
|
||||
&mesh.vertices[idx2]);
|
||||
|
||||
if (needsOriginalIndices) {
|
||||
meshBuilder.SetAttributeValuesForFace(originalIndexAttributeID, face,
|
||||
&mesh.originalIndices[idx0],
|
||||
&mesh.originalIndices[idx1],
|
||||
&mesh.originalIndices[idx2]);
|
||||
}
|
||||
if (hasNormals) {
|
||||
meshBuilder.SetAttributeValuesForFace(normalsAttributeID, face,
|
||||
&mesh.normals[idx0], &mesh.normals[idx1],
|
||||
&mesh.normals[idx2]);
|
||||
}
|
||||
if (hasColors) {
|
||||
meshBuilder.SetAttributeValuesForFace(colorsAttributeID, face,
|
||||
&mesh.colors[idx0], &mesh.colors[idx1],
|
||||
&mesh.colors[idx2]);
|
||||
}
|
||||
if (hasTexCoords) {
|
||||
meshBuilder.SetAttributeValuesForFace(texCoordsAttributeID, face,
|
||||
&mesh.texCoords[idx0], &mesh.texCoords[idx1],
|
||||
&mesh.texCoords[idx2]);
|
||||
}
|
||||
if (hasTexCoords1) {
|
||||
meshBuilder.SetAttributeValuesForFace(texCoords1AttributeID, face,
|
||||
&mesh.texCoords1[idx0], &mesh.texCoords1[idx1],
|
||||
&mesh.texCoords1[idx2]);
|
||||
}
|
||||
};
|
||||
|
||||
for (int i = 0; (i + 2) < part.quadTrianglesIndices.size(); i += 3) {
|
||||
addFace(part.quadTrianglesIndices, i, face++);
|
||||
}
|
||||
|
||||
for (int i = 0; (i + 2) < part.triangleIndices.size(); i += 3) {
|
||||
addFace(part.triangleIndices, i, face++);
|
||||
}
|
||||
|
||||
partIndex++;
|
||||
}
|
||||
|
||||
auto dracoMesh = meshBuilder.Finalize();
|
||||
|
||||
if (!dracoMesh) {
|
||||
handleWarning("Failed to finalize the baking of a draco Geometry node");
|
||||
continue;
|
||||
}
|
||||
|
||||
// we need to modify unique attribute IDs for custom attributes
|
||||
// so the attributes are easily retrievable on the other side
|
||||
if (hasPerFaceMaterials) {
|
||||
dracoMesh->attribute(faceMaterialAttributeID)->set_unique_id(DRACO_ATTRIBUTE_MATERIAL_ID);
|
||||
}
|
||||
|
||||
if (hasTexCoords1) {
|
||||
dracoMesh->attribute(texCoords1AttributeID)->set_unique_id(DRACO_ATTRIBUTE_TEX_COORD_1);
|
||||
}
|
||||
|
||||
if (needsOriginalIndices) {
|
||||
dracoMesh->attribute(originalIndexAttributeID)->set_unique_id(DRACO_ATTRIBUTE_ORIGINAL_INDEX);
|
||||
}
|
||||
|
||||
draco::Encoder encoder;
|
||||
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 14);
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 12);
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 10);
|
||||
encoder.SetSpeedOptions(0, 5);
|
||||
|
||||
draco::EncoderBuffer buffer;
|
||||
encoder.EncodeMeshToBuffer(*dracoMesh, &buffer);
|
||||
|
||||
FBXNode dracoMeshNode;
|
||||
dracoMeshNode.name = "DracoMesh";
|
||||
auto value = QVariant::fromValue(QByteArray(buffer.data(), (int) buffer.size()));
|
||||
dracoMeshNode.properties.append(value);
|
||||
|
||||
|
||||
objectChild.children.push_back(dracoMeshNode);
|
||||
|
||||
static const std::vector<QString> nodeNamesToDelete {
|
||||
|
@ -590,69 +316,25 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
|
|||
for (FBXNode& textureChild : object->children) {
|
||||
|
||||
if (textureChild.name == "RelativeFilename") {
|
||||
QString fbxTextureFileName { textureChild.properties.at(0).toString() };
|
||||
|
||||
// grab the ID for this texture so we can figure out the
|
||||
// texture type from the loaded materials
|
||||
auto textureID { object->properties[0].toString() };
|
||||
auto textureType = textureTypes[textureID];
|
||||
|
||||
// use QFileInfo to easily split up the existing texture filename into its components
|
||||
QString fbxTextureFileName { textureChild.properties.at(0).toByteArray() };
|
||||
QFileInfo textureFileInfo { fbxTextureFileName.replace("\\", "/") };
|
||||
// Compress the texture information and return the new filename to be added into the FBX scene
|
||||
auto bakedTextureFile = compressTexture(fbxTextureFileName, textureType);
|
||||
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (textureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
|
||||
// re-baking an FBX that already references baked textures is a fail
|
||||
// so we add an error and return from here
|
||||
handleError("Cannot re-bake a file that references compressed textures");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!image::getSupportedFormats().contains(textureFileInfo.suffix())) {
|
||||
// this is a texture format we don't bake, skip it
|
||||
handleWarning(fbxTextureFileName + " is not a bakeable texture format");
|
||||
continue;
|
||||
}
|
||||
|
||||
// make sure this texture points to something and isn't one we've already re-mapped
|
||||
if (!textureFileInfo.filePath().isEmpty()) {
|
||||
// check if this was an embedded texture we have already have in-memory content for
|
||||
auto textureContent = _textureContent.value(fbxTextureFileName.toLocal8Bit());
|
||||
|
||||
// figure out the URL to this texture, embedded or external
|
||||
auto urlToTexture = getTextureURL(textureFileInfo, fbxTextureFileName,
|
||||
!textureContent.isNull());
|
||||
|
||||
QString bakedTextureFileName;
|
||||
if (_remappedTexturePaths.contains(urlToTexture)) {
|
||||
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
|
||||
} else {
|
||||
// construct the new baked texture file name and file path
|
||||
// ensuring that the baked texture will have a unique name
|
||||
// even if there was another texture with the same name at a different path
|
||||
bakedTextureFileName = createBakedTextureFileName(textureFileInfo);
|
||||
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
|
||||
}
|
||||
|
||||
qCDebug(model_baking).noquote() << "Re-mapping" << fbxTextureFileName
|
||||
<< "to" << bakedTextureFileName;
|
||||
|
||||
QString bakedTextureFilePath {
|
||||
_bakedOutputDir + "/" + bakedTextureFileName
|
||||
};
|
||||
|
||||
// write the new filename into the FBX scene
|
||||
textureChild.properties[0] = bakedTextureFileName.toLocal8Bit();
|
||||
|
||||
if (!_bakingTextures.contains(urlToTexture)) {
|
||||
_outputFiles.push_back(bakedTextureFilePath);
|
||||
|
||||
// grab the ID for this texture so we can figure out the
|
||||
// texture type from the loaded materials
|
||||
QString textureID { object->properties[0].toByteArray() };
|
||||
auto textureType = textureTypes[textureID];
|
||||
|
||||
// bake this texture asynchronously
|
||||
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
|
||||
// If no errors or warnings have occurred during texture compression add the filename to the FBX scene
|
||||
if (!bakedTextureFile.isNull()) {
|
||||
textureChild.properties[0] = bakedTextureFile;
|
||||
} else {
|
||||
// if bake fails - return, if there were errors and continue, if there were warnings.
|
||||
if (hasErrors()) {
|
||||
return;
|
||||
} else if (hasWarnings()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -671,172 +353,26 @@ void FBXBaker::rewriteAndBakeSceneTextures() {
|
|||
}
|
||||
}
|
||||
|
||||
void FBXBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
|
||||
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
|
||||
// start a bake for this texture and add it to our list to keep track of
|
||||
QSharedPointer<TextureBaker> bakingTexture {
|
||||
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
|
||||
&TextureBaker::deleteLater
|
||||
};
|
||||
|
||||
// make sure we hear when the baking texture is done or aborted
|
||||
connect(bakingTexture.data(), &Baker::finished, this, &FBXBaker::handleBakedTexture);
|
||||
connect(bakingTexture.data(), &TextureBaker::aborted, this, &FBXBaker::handleAbortedTexture);
|
||||
|
||||
// keep a shared pointer to the baking texture
|
||||
_bakingTextures.insert(textureURL, bakingTexture);
|
||||
|
||||
// start baking the texture on one of our available worker threads
|
||||
bakingTexture->moveToThread(_textureThreadGetter());
|
||||
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
|
||||
}
|
||||
|
||||
void FBXBaker::handleBakedTexture() {
|
||||
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
|
||||
|
||||
// make sure we haven't already run into errors, and that this is a valid texture
|
||||
if (bakedTexture) {
|
||||
if (!shouldStop()) {
|
||||
if (!bakedTexture->hasErrors()) {
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
|
||||
|
||||
// use the path to the texture being baked to determine if this was an embedded or a linked texture
|
||||
|
||||
// it is embeddded if the texure being baked was inside a folder with the name of the FBX
|
||||
// since that is the fake URL we provide when baking external textures
|
||||
|
||||
if (!_fbxURL.isParentOf(bakedTexture->getTextureURL())) {
|
||||
// for linked textures we want to save a copy of original texture beside the original FBX
|
||||
|
||||
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
|
||||
|
||||
// check if we have a relative path to use for the texture
|
||||
auto relativeTexturePath = texturePathRelativeToFBX(_fbxURL, bakedTexture->getTextureURL());
|
||||
|
||||
QFile originalTextureFile {
|
||||
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
|
||||
};
|
||||
|
||||
if (relativeTexturePath.length() > 0) {
|
||||
// make the folders needed by the relative path
|
||||
}
|
||||
|
||||
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
|
||||
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
|
||||
<< "for" << _fbxURL;
|
||||
} else {
|
||||
handleError("Could not save original external texture " + originalTextureFile.fileName()
|
||||
+ " for " + _fbxURL.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
|
||||
checkIfTexturesFinished();
|
||||
} else {
|
||||
// there was an error baking this texture - add it to our list of errors
|
||||
_errorList.append(bakedTexture->getErrors());
|
||||
|
||||
// we don't emit finished yet so that the other textures can finish baking first
|
||||
_pendingErrorEmission = true;
|
||||
|
||||
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
|
||||
// abort any other ongoing texture bakes since we know we'll end up failing
|
||||
for (auto& bakingTexture : _bakingTextures) {
|
||||
bakingTexture->abort();
|
||||
}
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
} else {
|
||||
// we have errors to attend to, so we don't do extra processing for this texture
|
||||
// but we do need to remove that TextureBaker from our list
|
||||
// and then check if we're done with all textures
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::handleAbortedTexture() {
|
||||
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
|
||||
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
|
||||
|
||||
if (bakedTexture) {
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
}
|
||||
|
||||
// since a texture we were baking aborted, our status is also aborted
|
||||
_shouldAbort.store(true);
|
||||
|
||||
// abort any other ongoing texture bakes since we know we'll end up failing
|
||||
for (auto& bakingTexture : _bakingTextures) {
|
||||
bakingTexture->abort();
|
||||
}
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
|
||||
void FBXBaker::exportScene() {
|
||||
// save the relative path to this FBX inside our passed output folder
|
||||
auto fileName = _fbxURL.fileName();
|
||||
auto fileName = _modelURL.fileName();
|
||||
auto baseName = fileName.left(fileName.lastIndexOf('.'));
|
||||
auto bakedFilename = baseName + BAKED_FBX_EXTENSION;
|
||||
|
||||
_bakedFBXFilePath = _bakedOutputDir + "/" + bakedFilename;
|
||||
_bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename;
|
||||
|
||||
auto fbxData = FBXWriter::encodeFBX(_rootNode);
|
||||
|
||||
QFile bakedFile(_bakedFBXFilePath);
|
||||
QFile bakedFile(_bakedModelFilePath);
|
||||
|
||||
if (!bakedFile.open(QIODevice::WriteOnly)) {
|
||||
handleError("Error opening " + _bakedFBXFilePath + " for writing");
|
||||
handleError("Error opening " + _bakedModelFilePath + " for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
bakedFile.write(fbxData);
|
||||
|
||||
_outputFiles.push_back(_bakedFBXFilePath);
|
||||
_outputFiles.push_back(_bakedModelFilePath);
|
||||
|
||||
qCDebug(model_baking) << "Exported" << _fbxURL << "with re-written paths to" << _bakedFBXFilePath;
|
||||
}
|
||||
|
||||
void FBXBaker::checkIfTexturesFinished() {
|
||||
// check if we're done everything we need to do for this FBX
|
||||
// and emit our finished signal if we're done
|
||||
|
||||
if (_bakingTextures.isEmpty()) {
|
||||
if (shouldStop()) {
|
||||
// if we're checking for completion but we have errors
|
||||
// that means one or more of our texture baking operations failed
|
||||
|
||||
if (_pendingErrorEmission) {
|
||||
setIsFinished(true);
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
qCDebug(model_baking) << "Finished baking, emitting finsihed" << _fbxURL;
|
||||
|
||||
setIsFinished(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FBXBaker::setWasAborted(bool wasAborted) {
|
||||
if (wasAborted != _wasAborted.load()) {
|
||||
Baker::setWasAborted(wasAborted);
|
||||
|
||||
if (wasAborted) {
|
||||
qCDebug(model_baking) << "Aborted baking" << _fbxURL;
|
||||
}
|
||||
}
|
||||
qCDebug(model_baking) << "Exported" << _modelURL << "with re-written paths to" << _bakedModelFilePath;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
#include "Baker.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "ModelBaker.h"
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
|
||||
#include <gpu/Texture.h>
|
||||
|
@ -30,21 +30,13 @@ static const QString BAKED_FBX_EXTENSION = ".baked.fbx";
|
|||
|
||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
||||
|
||||
class FBXBaker : public Baker {
|
||||
class FBXBaker : public ModelBaker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
FBXBaker(const QUrl& fbxURL, TextureBakerThreadGetter textureThreadGetter,
|
||||
const QString& bakedOutputDir, const QString& originalOutputDir = "");
|
||||
~FBXBaker() override;
|
||||
|
||||
QUrl getFBXUrl() const { return _fbxURL; }
|
||||
QString getBakedFBXFilePath() const { return _bakedFBXFilePath; }
|
||||
|
||||
virtual void setWasAborted(bool wasAborted) override;
|
||||
using ModelBaker::ModelBaker;
|
||||
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
virtual void abort() override;
|
||||
|
||||
signals:
|
||||
void sourceCopyReadyToLoad();
|
||||
|
@ -52,8 +44,6 @@ signals:
|
|||
private slots:
|
||||
void bakeSourceCopy();
|
||||
void handleFBXNetworkReply();
|
||||
void handleBakedTexture();
|
||||
void handleAbortedTexture();
|
||||
|
||||
private:
|
||||
void setupOutputFolder();
|
||||
|
@ -64,38 +54,12 @@ private:
|
|||
void rewriteAndBakeSceneModels();
|
||||
void rewriteAndBakeSceneTextures();
|
||||
void exportScene();
|
||||
void removeEmbeddedMediaFolder();
|
||||
|
||||
void checkIfTexturesFinished();
|
||||
|
||||
QString createBakedTextureFileName(const QFileInfo& textureFileInfo);
|
||||
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
|
||||
|
||||
void bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType, const QDir& outputDir,
|
||||
const QString& bakedFilename, const QByteArray& textureContent = QByteArray());
|
||||
|
||||
QUrl _fbxURL;
|
||||
|
||||
FBXNode _rootNode;
|
||||
FBXGeometry* _geometry;
|
||||
QHash<QByteArray, QByteArray> _textureContent;
|
||||
|
||||
QString _bakedFBXFilePath;
|
||||
|
||||
QString _bakedOutputDir;
|
||||
|
||||
// If set, the original FBX and textures will also be copied here
|
||||
QString _originalOutputDir;
|
||||
|
||||
QDir _tempDir;
|
||||
QString _originalFBXFilePath;
|
||||
|
||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||
QHash<QString, int> _textureNameMatchCount;
|
||||
QHash<QUrl, QString> _remappedTexturePaths;
|
||||
|
||||
TextureBakerThreadGetter _textureThreadGetter;
|
||||
|
||||
bool _pendingErrorEmission { false };
|
||||
};
|
||||
|
||||
|
|
521
libraries/baking/src/ModelBaker.cpp
Normal file
521
libraries/baking/src/ModelBaker.cpp
Normal file
|
@ -0,0 +1,521 @@
|
|||
//
|
||||
// ModelBaker.cpp
|
||||
// libraries/baking/src
|
||||
//
|
||||
// Created by Utkarsh Gautam on 9/29/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "ModelBaker.h"
|
||||
|
||||
#include <PathUtils.h>
|
||||
|
||||
#include <FBXReader.h>
|
||||
#include <FBXWriter.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( push )
|
||||
#pragma warning( disable : 4267 )
|
||||
#endif
|
||||
|
||||
#include <draco/mesh/triangle_soup_mesh_builder.h>
|
||||
#include <draco/compression/encode.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#pragma warning( pop )
|
||||
#endif
|
||||
|
||||
ModelBaker::ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
|
||||
const QString& bakedOutputDirectory, const QString& originalOutputDirectory) :
|
||||
_modelURL(inputModelURL),
|
||||
_bakedOutputDir(bakedOutputDirectory),
|
||||
_originalOutputDir(originalOutputDirectory),
|
||||
_textureThreadGetter(inputTextureThreadGetter)
|
||||
{
|
||||
auto tempDir = PathUtils::generateTemporaryDir();
|
||||
|
||||
if (tempDir.isEmpty()) {
|
||||
handleError("Failed to create a temporary directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
_modelTempDir = tempDir;
|
||||
|
||||
_originalModelFilePath = _modelTempDir.filePath(_modelURL.fileName());
|
||||
qDebug() << "Made temporary dir " << _modelTempDir;
|
||||
qDebug() << "Origin file path: " << _originalModelFilePath;
|
||||
|
||||
}
|
||||
|
||||
ModelBaker::~ModelBaker() {
|
||||
if (_modelTempDir.exists()) {
|
||||
if (!_modelTempDir.remove(_originalModelFilePath)) {
|
||||
qCWarning(model_baking) << "Failed to remove temporary copy of fbx file:" << _originalModelFilePath;
|
||||
}
|
||||
if (!_modelTempDir.rmdir(".")) {
|
||||
qCWarning(model_baking) << "Failed to remove temporary directory:" << _modelTempDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModelBaker::abort() {
|
||||
Baker::abort();
|
||||
|
||||
// tell our underlying TextureBaker instances to abort
|
||||
// the ModelBaker will wait until all are aborted before emitting its own abort signal
|
||||
for (auto& textureBaker : _bakingTextures) {
|
||||
textureBaker->abort();
|
||||
}
|
||||
}
|
||||
|
||||
bool ModelBaker::compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback) {
|
||||
if (mesh.wasCompressed) {
|
||||
handleError("Cannot re-bake a file that contains compressed mesh");
|
||||
return false;
|
||||
}
|
||||
|
||||
Q_ASSERT(mesh.normals.size() == 0 || mesh.normals.size() == mesh.vertices.size());
|
||||
Q_ASSERT(mesh.colors.size() == 0 || mesh.colors.size() == mesh.vertices.size());
|
||||
Q_ASSERT(mesh.texCoords.size() == 0 || mesh.texCoords.size() == mesh.vertices.size());
|
||||
|
||||
int64_t numTriangles{ 0 };
|
||||
for (auto& part : mesh.parts) {
|
||||
if ((part.quadTrianglesIndices.size() % 3) != 0 || (part.triangleIndices.size() % 3) != 0) {
|
||||
handleWarning("Found a mesh part with invalid index data, skipping");
|
||||
continue;
|
||||
}
|
||||
numTriangles += part.quadTrianglesIndices.size() / 3;
|
||||
numTriangles += part.triangleIndices.size() / 3;
|
||||
}
|
||||
|
||||
if (numTriangles == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
draco::TriangleSoupMeshBuilder meshBuilder;
|
||||
|
||||
meshBuilder.Start(numTriangles);
|
||||
|
||||
bool hasNormals{ mesh.normals.size() > 0 };
|
||||
bool hasColors{ mesh.colors.size() > 0 };
|
||||
bool hasTexCoords{ mesh.texCoords.size() > 0 };
|
||||
bool hasTexCoords1{ mesh.texCoords1.size() > 0 };
|
||||
bool hasPerFaceMaterials = (materialIDCallback) ? (mesh.parts.size() > 1 || materialIDCallback(0) != 0 ) : true;
|
||||
bool needsOriginalIndices{ hasDeformers };
|
||||
|
||||
int normalsAttributeID { -1 };
|
||||
int colorsAttributeID { -1 };
|
||||
int texCoordsAttributeID { -1 };
|
||||
int texCoords1AttributeID { -1 };
|
||||
int faceMaterialAttributeID { -1 };
|
||||
int originalIndexAttributeID { -1 };
|
||||
|
||||
const int positionAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::POSITION,
|
||||
3, draco::DT_FLOAT32);
|
||||
if (needsOriginalIndices) {
|
||||
originalIndexAttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_ORIGINAL_INDEX,
|
||||
1, draco::DT_INT32);
|
||||
}
|
||||
|
||||
if (hasNormals) {
|
||||
normalsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::NORMAL,
|
||||
3, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasColors) {
|
||||
colorsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::COLOR,
|
||||
3, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasTexCoords) {
|
||||
texCoordsAttributeID = meshBuilder.AddAttribute(draco::GeometryAttribute::TEX_COORD,
|
||||
2, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasTexCoords1) {
|
||||
texCoords1AttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_TEX_COORD_1,
|
||||
2, draco::DT_FLOAT32);
|
||||
}
|
||||
if (hasPerFaceMaterials) {
|
||||
faceMaterialAttributeID = meshBuilder.AddAttribute(
|
||||
(draco::GeometryAttribute::Type)DRACO_ATTRIBUTE_MATERIAL_ID,
|
||||
1, draco::DT_UINT16);
|
||||
}
|
||||
|
||||
auto partIndex = 0;
|
||||
draco::FaceIndex face;
|
||||
uint16_t materialID;
|
||||
|
||||
for (auto& part : mesh.parts) {
|
||||
materialID = (materialIDCallback) ? materialIDCallback(partIndex) : partIndex;
|
||||
|
||||
auto addFace = [&](QVector<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);
|
||||
|
||||
dracoMeshNode = dracoNode;
|
||||
// Mesh compression successful return true
|
||||
return true;
|
||||
}
|
||||
|
||||
QString ModelBaker::compressTexture(QString modelTextureFileName, image::TextureUsage::Type textureType) {
|
||||
|
||||
QFileInfo modelTextureFileInfo{ modelTextureFileName.replace("\\", "/") };
|
||||
|
||||
if (modelTextureFileInfo.suffix() == BAKED_TEXTURE_EXT.mid(1)) {
|
||||
// re-baking a model that already references baked textures
|
||||
// this is an error - return from here
|
||||
handleError("Cannot re-bake a file that already references compressed textures");
|
||||
return QString::null;
|
||||
}
|
||||
|
||||
if (!image::getSupportedFormats().contains(modelTextureFileInfo.suffix())) {
|
||||
// this is a texture format we don't bake, skip it
|
||||
handleWarning(modelTextureFileName + " is not a bakeable texture format");
|
||||
return QString::null;
|
||||
}
|
||||
|
||||
// make sure this texture points to something and isn't one we've already re-mapped
|
||||
QString textureChild { QString::null };
|
||||
if (!modelTextureFileInfo.filePath().isEmpty()) {
|
||||
// check if this was an embedded texture that we already have in-memory content for
|
||||
QByteArray textureContent;
|
||||
|
||||
// figure out the URL to this texture, embedded or external
|
||||
if (!modelTextureFileInfo.filePath().isEmpty()) {
|
||||
textureContent = _textureContentMap.value(modelTextureFileName.toLocal8Bit());
|
||||
}
|
||||
auto urlToTexture = getTextureURL(modelTextureFileInfo, modelTextureFileName, !textureContent.isNull());
|
||||
|
||||
QString bakedTextureFileName;
|
||||
if (_remappedTexturePaths.contains(urlToTexture)) {
|
||||
bakedTextureFileName = _remappedTexturePaths[urlToTexture];
|
||||
} else {
|
||||
// construct the new baked texture file name and file path
|
||||
// ensuring that the baked texture will have a unique name
|
||||
// even if there was another texture with the same name at a different path
|
||||
bakedTextureFileName = createBakedTextureFileName(modelTextureFileInfo);
|
||||
_remappedTexturePaths[urlToTexture] = bakedTextureFileName;
|
||||
}
|
||||
|
||||
qCDebug(model_baking).noquote() << "Re-mapping" << modelTextureFileName
|
||||
<< "to" << bakedTextureFileName;
|
||||
|
||||
QString bakedTextureFilePath{
|
||||
_bakedOutputDir + "/" + bakedTextureFileName
|
||||
};
|
||||
|
||||
textureChild = bakedTextureFileName;
|
||||
|
||||
if (!_bakingTextures.contains(urlToTexture)) {
|
||||
_outputFiles.push_back(bakedTextureFilePath);
|
||||
|
||||
// bake this texture asynchronously
|
||||
bakeTexture(urlToTexture, textureType, _bakedOutputDir, bakedTextureFileName, textureContent);
|
||||
}
|
||||
}
|
||||
|
||||
return textureChild;
|
||||
}
|
||||
|
||||
void ModelBaker::bakeTexture(const QUrl& textureURL, image::TextureUsage::Type textureType,
|
||||
const QDir& outputDir, const QString& bakedFilename, const QByteArray& textureContent) {
|
||||
|
||||
// start a bake for this texture and add it to our list to keep track of
|
||||
QSharedPointer<TextureBaker> bakingTexture{
|
||||
new TextureBaker(textureURL, textureType, outputDir, bakedFilename, textureContent),
|
||||
&TextureBaker::deleteLater
|
||||
};
|
||||
|
||||
// make sure we hear when the baking texture is done or aborted
|
||||
connect(bakingTexture.data(), &Baker::finished, this, &ModelBaker::handleBakedTexture);
|
||||
connect(bakingTexture.data(), &TextureBaker::aborted, this, &ModelBaker::handleAbortedTexture);
|
||||
|
||||
// keep a shared pointer to the baking texture
|
||||
_bakingTextures.insert(textureURL, bakingTexture);
|
||||
|
||||
// start baking the texture on one of our available worker threads
|
||||
bakingTexture->moveToThread(_textureThreadGetter());
|
||||
QMetaObject::invokeMethod(bakingTexture.data(), "bake");
|
||||
}
|
||||
|
||||
void ModelBaker::handleBakedTexture() {
|
||||
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
|
||||
qDebug() << "Handling baked texture" << bakedTexture->getTextureURL();
|
||||
|
||||
// make sure we haven't already run into errors, and that this is a valid texture
|
||||
if (bakedTexture) {
|
||||
if (!shouldStop()) {
|
||||
if (!bakedTexture->hasErrors()) {
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
// we've been asked to make copies of the originals, so we need to make copies of this if it is a linked texture
|
||||
|
||||
// use the path to the texture being baked to determine if this was an embedded or a linked texture
|
||||
|
||||
// it is embeddded if the texure being baked was inside a folder with the name of the model
|
||||
// since that is the fake URL we provide when baking external textures
|
||||
|
||||
if (!_modelURL.isParentOf(bakedTexture->getTextureURL())) {
|
||||
// for linked textures we want to save a copy of original texture beside the original model
|
||||
|
||||
qCDebug(model_baking) << "Saving original texture for" << bakedTexture->getTextureURL();
|
||||
|
||||
// check if we have a relative path to use for the texture
|
||||
auto relativeTexturePath = texturePathRelativeToModel(_modelURL, bakedTexture->getTextureURL());
|
||||
|
||||
QFile originalTextureFile{
|
||||
_originalOutputDir + "/" + relativeTexturePath + bakedTexture->getTextureURL().fileName()
|
||||
};
|
||||
|
||||
if (relativeTexturePath.length() > 0) {
|
||||
// make the folders needed by the relative path
|
||||
}
|
||||
|
||||
if (originalTextureFile.open(QIODevice::WriteOnly) && originalTextureFile.write(bakedTexture->getOriginalTexture()) != -1) {
|
||||
qCDebug(model_baking) << "Saved original texture file" << originalTextureFile.fileName()
|
||||
<< "for" << _modelURL;
|
||||
} else {
|
||||
handleError("Could not save original external texture " + originalTextureFile.fileName()
|
||||
+ " for " + _modelURL.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// now that this texture has been baked and handled, we can remove that TextureBaker from our hash
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
|
||||
checkIfTexturesFinished();
|
||||
} else {
|
||||
// there was an error baking this texture - add it to our list of errors
|
||||
_errorList.append(bakedTexture->getErrors());
|
||||
|
||||
// we don't emit finished yet so that the other textures can finish baking first
|
||||
_pendingErrorEmission = true;
|
||||
|
||||
// now that this texture has been baked, even though it failed, we can remove that TextureBaker from our list
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
|
||||
// abort any other ongoing texture bakes since we know we'll end up failing
|
||||
for (auto& bakingTexture : _bakingTextures) {
|
||||
bakingTexture->abort();
|
||||
}
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
} else {
|
||||
// we have errors to attend to, so we don't do extra processing for this texture
|
||||
// but we do need to remove that TextureBaker from our list
|
||||
// and then check if we're done with all textures
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModelBaker::handleAbortedTexture() {
|
||||
// grab the texture bake that was aborted and remove it from our hash since we don't need to track it anymore
|
||||
TextureBaker* bakedTexture = qobject_cast<TextureBaker*>(sender());
|
||||
|
||||
qDebug() << "Texture aborted: " << bakedTexture->getTextureURL();
|
||||
|
||||
if (bakedTexture) {
|
||||
_bakingTextures.remove(bakedTexture->getTextureURL());
|
||||
}
|
||||
|
||||
// since a texture we were baking aborted, our status is also aborted
|
||||
_shouldAbort.store(true);
|
||||
|
||||
// abort any other ongoing texture bakes since we know we'll end up failing
|
||||
for (auto& bakingTexture : _bakingTextures) {
|
||||
bakingTexture->abort();
|
||||
}
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
|
||||
QUrl ModelBaker::getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded) {
|
||||
QUrl urlToTexture;
|
||||
|
||||
// use QFileInfo to easily split up the existing texture filename into its components
|
||||
auto apparentRelativePath = QFileInfo(relativeFileName.replace("\\", "/"));
|
||||
|
||||
if (isEmbedded) {
|
||||
urlToTexture = _modelURL.toString() + "/" + apparentRelativePath.filePath();
|
||||
} else {
|
||||
if (textureFileInfo.exists() && textureFileInfo.isFile()) {
|
||||
// set the texture URL to the local texture that we have confirmed exists
|
||||
urlToTexture = QUrl::fromLocalFile(textureFileInfo.absoluteFilePath());
|
||||
} else {
|
||||
// external texture that we'll need to download or find
|
||||
|
||||
// this is a relative file path which will require different handling
|
||||
// depending on the location of the original model
|
||||
if (_modelURL.isLocalFile() && apparentRelativePath.exists() && apparentRelativePath.isFile()) {
|
||||
// the absolute path we ran into for the texture in the model exists on this machine
|
||||
// so use that file
|
||||
urlToTexture = QUrl::fromLocalFile(apparentRelativePath.absoluteFilePath());
|
||||
} else {
|
||||
// we didn't find the texture on this machine at the absolute path
|
||||
// so assume that it is right beside the model to match the behaviour of interface
|
||||
urlToTexture = _modelURL.resolved(apparentRelativePath.fileName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urlToTexture;
|
||||
}
|
||||
|
||||
QString ModelBaker::texturePathRelativeToModel(QUrl modelURL, QUrl textureURL) {
|
||||
auto modelPath = modelURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
auto texturePath = textureURL.toString(QUrl::RemoveFilename | QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
|
||||
if (texturePath.startsWith(modelPath)) {
|
||||
// texture path is a child of the model path, return the texture path without the model path
|
||||
return texturePath.mid(modelPath.length());
|
||||
} else {
|
||||
// the texture path was not a child of the model path, return the empty string
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
void ModelBaker::checkIfTexturesFinished() {
|
||||
// check if we're done everything we need to do for this model
|
||||
// and emit our finished signal if we're done
|
||||
|
||||
if (_bakingTextures.isEmpty()) {
|
||||
if (shouldStop()) {
|
||||
// if we're checking for completion but we have errors
|
||||
// that means one or more of our texture baking operations failed
|
||||
|
||||
if (_pendingErrorEmission) {
|
||||
setIsFinished(true);
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
qCDebug(model_baking) << "Finished baking, emitting finished" << _modelURL;
|
||||
|
||||
setIsFinished(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString ModelBaker::createBakedTextureFileName(const QFileInfo& textureFileInfo) {
|
||||
// first make sure we have a unique base name for this texture
|
||||
// in case another texture referenced by this model has the same base name
|
||||
auto& nameMatches = _textureNameMatchCount[textureFileInfo.baseName()];
|
||||
|
||||
QString bakedTextureFileName{ textureFileInfo.completeBaseName() };
|
||||
|
||||
if (nameMatches > 0) {
|
||||
// there are already nameMatches texture with this name
|
||||
// append - and that number to our baked texture file name so that it is unique
|
||||
bakedTextureFileName += "-" + QString::number(nameMatches);
|
||||
}
|
||||
|
||||
bakedTextureFileName += BAKED_TEXTURE_EXT;
|
||||
|
||||
// increment the number of name matches
|
||||
++nameMatches;
|
||||
|
||||
return bakedTextureFileName;
|
||||
}
|
||||
|
||||
void ModelBaker::setWasAborted(bool wasAborted) {
|
||||
if (wasAborted != _wasAborted.load()) {
|
||||
Baker::setWasAborted(wasAborted);
|
||||
|
||||
if (wasAborted) {
|
||||
qCDebug(model_baking) << "Aborted baking" << _modelURL;
|
||||
}
|
||||
}
|
||||
}
|
79
libraries/baking/src/ModelBaker.h
Normal file
79
libraries/baking/src/ModelBaker.h
Normal file
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// ModelBaker.h
|
||||
// libraries/baking/src
|
||||
//
|
||||
// Created by Utkarsh Gautam on 9/29/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_ModelBaker_h
|
||||
#define hifi_ModelBaker_h
|
||||
|
||||
#include <QtCore/QFutureSynchronizer>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QUrl>
|
||||
#include <QtNetwork/QNetworkReply>
|
||||
|
||||
#include "Baker.h"
|
||||
#include "TextureBaker.h"
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
|
||||
#include <gpu/Texture.h>
|
||||
|
||||
#include <FBX.h>
|
||||
|
||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
||||
using GetMaterialIDCallback = std::function <int(int)>;
|
||||
|
||||
class ModelBaker : public Baker {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ModelBaker(const QUrl& inputModelURL, TextureBakerThreadGetter inputTextureThreadGetter,
|
||||
const QString& bakedOutputDirectory, const QString& originalOutputDirectory = "");
|
||||
virtual ~ModelBaker();
|
||||
|
||||
bool compressMesh(FBXMesh& mesh, bool hasDeformers, FBXNode& dracoMeshNode, GetMaterialIDCallback materialIDCallback = nullptr);
|
||||
QString compressTexture(QString textureFileName, image::TextureUsage::Type = image::TextureUsage::Type::DEFAULT_TEXTURE);
|
||||
virtual void setWasAborted(bool wasAborted) override;
|
||||
|
||||
QUrl getModelURL() const { return _modelURL; }
|
||||
QString getBakedModelFilePath() const { return _bakedModelFilePath; }
|
||||
|
||||
public slots:
|
||||
virtual void abort() override;
|
||||
|
||||
protected:
|
||||
void checkIfTexturesFinished();
|
||||
|
||||
QHash<QByteArray, QByteArray> _textureContentMap;
|
||||
QUrl _modelURL;
|
||||
QString _bakedOutputDir;
|
||||
QString _originalOutputDir;
|
||||
QString _bakedModelFilePath;
|
||||
QDir _modelTempDir;
|
||||
QString _originalModelFilePath;
|
||||
|
||||
private slots:
|
||||
void handleBakedTexture();
|
||||
void handleAbortedTexture();
|
||||
|
||||
private:
|
||||
QString createBakedTextureFileName(const QFileInfo & textureFileInfo);
|
||||
QUrl getTextureURL(const QFileInfo& textureFileInfo, QString relativeFileName, bool isEmbedded = false);
|
||||
void bakeTexture(const QUrl & textureURL, image::TextureUsage::Type textureType, const QDir & outputDir,
|
||||
const QString & bakedFilename, const QByteArray & textureContent);
|
||||
QString texturePathRelativeToModel(QUrl modelURL, QUrl textureURL);
|
||||
|
||||
TextureBakerThreadGetter _textureThreadGetter;
|
||||
QMultiHash<QUrl, QSharedPointer<TextureBaker>> _bakingTextures;
|
||||
QHash<QString, int> _textureNameMatchCount;
|
||||
QHash<QUrl, QString> _remappedTexturePaths;
|
||||
bool _pendingErrorEmission{ false };
|
||||
};
|
||||
|
||||
#endif // hifi_ModelBaker_h
|
404
libraries/baking/src/OBJBaker.cpp
Normal file
404
libraries/baking/src/OBJBaker.cpp
Normal file
|
@ -0,0 +1,404 @@
|
|||
//
|
||||
// OBJBaker.cpp
|
||||
// libraries/baking/src
|
||||
//
|
||||
// Created by Utkarsh Gautam on 9/29/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include <PathUtils.h>
|
||||
#include <NetworkAccessManager.h>
|
||||
|
||||
#include "OBJBaker.h"
|
||||
#include "OBJReader.h"
|
||||
#include "FBXWriter.h"
|
||||
|
||||
const double UNIT_SCALE_FACTOR = 100.0;
|
||||
const QByteArray PROPERTIES70_NODE_NAME = "Properties70";
|
||||
const QByteArray P_NODE_NAME = "P";
|
||||
const QByteArray C_NODE_NAME = "C";
|
||||
const QByteArray FBX_HEADER_EXTENSION = "FBXHeaderExtension";
|
||||
const QByteArray GLOBAL_SETTINGS_NODE_NAME = "GlobalSettings";
|
||||
const QByteArray OBJECTS_NODE_NAME = "Objects";
|
||||
const QByteArray GEOMETRY_NODE_NAME = "Geometry";
|
||||
const QByteArray MODEL_NODE_NAME = "Model";
|
||||
const QByteArray MATERIAL_NODE_NAME = "Material";
|
||||
const QByteArray TEXTURE_NODE_NAME = "Texture";
|
||||
const QByteArray TEXTURENAME_NODE_NAME = "TextureName";
|
||||
const QByteArray RELATIVEFILENAME_NODE_NAME = "RelativeFilename";
|
||||
const QByteArray CONNECTIONS_NODE_NAME = "Connections";
|
||||
const QByteArray CONNECTIONS_NODE_PROPERTY = "OO";
|
||||
const QByteArray CONNECTIONS_NODE_PROPERTY_1 = "OP";
|
||||
const QByteArray MESH = "Mesh";
|
||||
|
||||
void OBJBaker::bake() {
|
||||
qDebug() << "OBJBaker" << _modelURL << "bake starting";
|
||||
|
||||
// trigger bakeOBJ once OBJ is loaded
|
||||
connect(this, &OBJBaker::OBJLoaded, this, &OBJBaker::bakeOBJ);
|
||||
|
||||
// make a local copy of the OBJ
|
||||
loadOBJ();
|
||||
}
|
||||
|
||||
void OBJBaker::loadOBJ() {
|
||||
if (!QDir().mkpath(_bakedOutputDir)) {
|
||||
handleError("Failed to create baked OBJ output folder " + _bakedOutputDir);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QDir().mkpath(_originalOutputDir)) {
|
||||
handleError("Failed to create original OBJ output folder " + _originalOutputDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the OBJ is local or it needs to be downloaded
|
||||
if (_modelURL.isLocalFile()) {
|
||||
// loading the local OBJ
|
||||
QFile localOBJ { _modelURL.toLocalFile() };
|
||||
|
||||
qDebug() << "Local file url: " << _modelURL << _modelURL.toString() << _modelURL.toLocalFile() << ", copying to: " << _originalModelFilePath;
|
||||
|
||||
if (!localOBJ.exists()) {
|
||||
handleError("Could not find " + _modelURL.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// make a copy in the output folder
|
||||
if (!_originalOutputDir.isEmpty()) {
|
||||
qDebug() << "Copying to: " << _originalOutputDir << "/" << _modelURL.fileName();
|
||||
localOBJ.copy(_originalOutputDir + "/" + _modelURL.fileName());
|
||||
}
|
||||
|
||||
localOBJ.copy(_originalModelFilePath);
|
||||
|
||||
// local OBJ is loaded emit signal to trigger its baking
|
||||
emit OBJLoaded();
|
||||
} else {
|
||||
// OBJ is remote, start download
|
||||
auto& networkAccessManager = NetworkAccessManager::getInstance();
|
||||
|
||||
QNetworkRequest networkRequest;
|
||||
|
||||
// setup the request to follow re-directs and always hit the network
|
||||
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
|
||||
networkRequest.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
|
||||
networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
|
||||
networkRequest.setUrl(_modelURL);
|
||||
|
||||
qCDebug(model_baking) << "Downloading" << _modelURL;
|
||||
auto networkReply = networkAccessManager.get(networkRequest);
|
||||
|
||||
connect(networkReply, &QNetworkReply::finished, this, &OBJBaker::handleOBJNetworkReply);
|
||||
}
|
||||
}
|
||||
|
||||
void OBJBaker::handleOBJNetworkReply() {
|
||||
auto requestReply = qobject_cast<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();
|
||||
|
||||
bool combineParts = true; // set true so that OBJReader reads material info from material library
|
||||
OBJReader reader;
|
||||
auto geometry = reader.readOBJ(objData, QVariantHash(), combineParts, _modelURL);
|
||||
|
||||
// Write OBJ Data as FBX tree nodes
|
||||
FBXNode rootNode;
|
||||
createFBXNodeTree(rootNode, *geometry);
|
||||
|
||||
// Serialize the resultant FBX tree
|
||||
auto encodedFBX = FBXWriter::encodeFBX(rootNode);
|
||||
|
||||
// Export as baked FBX
|
||||
auto fileName = _modelURL.fileName();
|
||||
auto baseName = fileName.left(fileName.lastIndexOf('.'));
|
||||
auto bakedFilename = baseName + ".baked.fbx";
|
||||
|
||||
_bakedModelFilePath = _bakedOutputDir + "/" + bakedFilename;
|
||||
|
||||
QFile bakedFile;
|
||||
bakedFile.setFileName(_bakedModelFilePath);
|
||||
if (!bakedFile.open(QIODevice::WriteOnly)) {
|
||||
handleError("Error opening " + _bakedModelFilePath + " for writing");
|
||||
return;
|
||||
}
|
||||
|
||||
bakedFile.write(encodedFBX);
|
||||
|
||||
// Export successful
|
||||
_outputFiles.push_back(_bakedModelFilePath);
|
||||
qCDebug(model_baking) << "Exported" << _modelURL << "to" << _bakedModelFilePath;
|
||||
|
||||
checkIfTexturesFinished();
|
||||
}
|
||||
|
||||
void OBJBaker::createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry) {
|
||||
// Generating FBX Header Node
|
||||
FBXNode headerNode;
|
||||
headerNode.name = FBX_HEADER_EXTENSION;
|
||||
|
||||
// Generating global settings node
|
||||
// Required for Unit Scale Factor
|
||||
FBXNode globalSettingsNode;
|
||||
globalSettingsNode.name = GLOBAL_SETTINGS_NODE_NAME;
|
||||
|
||||
// Setting the tree hierarchy: GlobalSettings -> Properties70 -> P -> Properties
|
||||
FBXNode properties70Node;
|
||||
properties70Node.name = PROPERTIES70_NODE_NAME;
|
||||
|
||||
FBXNode pNode;
|
||||
{
|
||||
pNode.name = P_NODE_NAME;
|
||||
pNode.properties.append({
|
||||
"UnitScaleFactor", "double", "Number", "",
|
||||
UNIT_SCALE_FACTOR
|
||||
});
|
||||
}
|
||||
|
||||
properties70Node.children = { pNode };
|
||||
globalSettingsNode.children = { properties70Node };
|
||||
|
||||
// Generating Object node
|
||||
_objectNode.name = OBJECTS_NODE_NAME;
|
||||
|
||||
// Generating Object node's child - Geometry node
|
||||
FBXNode geometryNode;
|
||||
geometryNode.name = GEOMETRY_NODE_NAME;
|
||||
{
|
||||
_geometryID = nextNodeID();
|
||||
geometryNode.properties = {
|
||||
_geometryID,
|
||||
GEOMETRY_NODE_NAME,
|
||||
MESH
|
||||
};
|
||||
}
|
||||
|
||||
// Compress the mesh information and store in dracoNode
|
||||
bool hasDeformers = false; // No concept of deformers for an OBJ
|
||||
FBXNode dracoNode;
|
||||
compressMesh(geometry.meshes[0], hasDeformers, dracoNode);
|
||||
geometryNode.children.append(dracoNode);
|
||||
|
||||
// Generating Object node's child - Model node
|
||||
FBXNode modelNode;
|
||||
modelNode.name = MODEL_NODE_NAME;
|
||||
{
|
||||
_modelID = nextNodeID();
|
||||
modelNode.properties = { _modelID, MODEL_NODE_NAME, MESH };
|
||||
}
|
||||
|
||||
_objectNode.children = { geometryNode, modelNode };
|
||||
|
||||
// Generating Objects node's child - Material node
|
||||
auto& meshParts = geometry.meshes[0].parts;
|
||||
for (auto& meshPart : meshParts) {
|
||||
FBXNode materialNode;
|
||||
materialNode.name = MATERIAL_NODE_NAME;
|
||||
if (geometry.materials.size() == 1) {
|
||||
// case when no material information is provided, OBJReader considers it as a single default material
|
||||
for (auto& materialID : geometry.materials.keys()) {
|
||||
setMaterialNodeProperties(materialNode, materialID, geometry);
|
||||
}
|
||||
} else {
|
||||
setMaterialNodeProperties(materialNode, meshPart.materialID, geometry);
|
||||
}
|
||||
|
||||
_objectNode.children.append(materialNode);
|
||||
}
|
||||
|
||||
// Generating Texture Node
|
||||
// iterate through mesh parts and process the associated textures
|
||||
auto size = meshParts.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
QString material = meshParts[i].materialID;
|
||||
FBXMaterial currentMaterial = geometry.materials[material];
|
||||
if (!currentMaterial.albedoTexture.filename.isEmpty() || !currentMaterial.specularTexture.filename.isEmpty()) {
|
||||
_textureID = nextNodeID();
|
||||
_mapTextureMaterial.emplace_back(_textureID, i);
|
||||
|
||||
FBXNode textureNode;
|
||||
{
|
||||
textureNode.name = TEXTURE_NODE_NAME;
|
||||
textureNode.properties = { _textureID };
|
||||
}
|
||||
|
||||
// Texture node child - TextureName node
|
||||
FBXNode textureNameNode;
|
||||
{
|
||||
textureNameNode.name = TEXTURENAME_NODE_NAME;
|
||||
QByteArray propertyString = (!currentMaterial.albedoTexture.filename.isEmpty()) ? "Kd" : "Ka";
|
||||
textureNameNode.properties = { propertyString };
|
||||
}
|
||||
|
||||
// Texture node child - Relative Filename node
|
||||
FBXNode relativeFilenameNode;
|
||||
{
|
||||
relativeFilenameNode.name = RELATIVEFILENAME_NODE_NAME;
|
||||
}
|
||||
|
||||
QByteArray textureFileName = (!currentMaterial.albedoTexture.filename.isEmpty()) ? currentMaterial.albedoTexture.filename : currentMaterial.specularTexture.filename;
|
||||
|
||||
auto textureType = (!currentMaterial.albedoTexture.filename.isEmpty()) ? image::TextureUsage::Type::ALBEDO_TEXTURE : image::TextureUsage::Type::SPECULAR_TEXTURE;
|
||||
|
||||
// Compress the texture using ModelBaker::compressTexture() and store compressed file's name in the node
|
||||
auto textureFile = compressTexture(textureFileName, textureType);
|
||||
if (textureFile.isNull()) {
|
||||
// Baking failed return
|
||||
handleError("Failed to compress texture: " + textureFileName);
|
||||
return;
|
||||
}
|
||||
relativeFilenameNode.properties = { textureFile };
|
||||
|
||||
textureNode.children = { textureNameNode, relativeFilenameNode };
|
||||
|
||||
_objectNode.children.append(textureNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Generating Connections node
|
||||
FBXNode connectionsNode;
|
||||
connectionsNode.name = CONNECTIONS_NODE_NAME;
|
||||
|
||||
// connect Geometry to Model
|
||||
FBXNode cNode;
|
||||
cNode.name = C_NODE_NAME;
|
||||
cNode.properties = { CONNECTIONS_NODE_PROPERTY, _geometryID, _modelID };
|
||||
connectionsNode.children = { cNode };
|
||||
|
||||
// connect all materials to model
|
||||
for (auto& materialID : _materialIDs) {
|
||||
FBXNode cNode;
|
||||
cNode.name = C_NODE_NAME;
|
||||
cNode.properties = { CONNECTIONS_NODE_PROPERTY, materialID, _modelID };
|
||||
connectionsNode.children.append(cNode);
|
||||
}
|
||||
|
||||
// Connect textures to materials
|
||||
for (const auto& texMat : _mapTextureMaterial) {
|
||||
FBXNode cAmbientNode;
|
||||
cAmbientNode.name = C_NODE_NAME;
|
||||
cAmbientNode.properties = {
|
||||
CONNECTIONS_NODE_PROPERTY_1,
|
||||
texMat.first,
|
||||
_materialIDs[texMat.second],
|
||||
"AmbientFactor"
|
||||
};
|
||||
connectionsNode.children.append(cAmbientNode);
|
||||
|
||||
FBXNode cDiffuseNode;
|
||||
cDiffuseNode.name = C_NODE_NAME;
|
||||
cDiffuseNode.properties = {
|
||||
CONNECTIONS_NODE_PROPERTY_1,
|
||||
texMat.first,
|
||||
_materialIDs[texMat.second],
|
||||
"DiffuseColor"
|
||||
};
|
||||
connectionsNode.children.append(cDiffuseNode);
|
||||
}
|
||||
|
||||
// Make all generated nodes children of rootNode
|
||||
rootNode.children = { globalSettingsNode, _objectNode, connectionsNode };
|
||||
}
|
||||
|
||||
// Set properties for material nodes
|
||||
void OBJBaker::setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry) {
|
||||
auto materialID = nextNodeID();
|
||||
_materialIDs.push_back(materialID);
|
||||
materialNode.properties = { materialID, material, MESH };
|
||||
|
||||
FBXMaterial currentMaterial = geometry.materials[material];
|
||||
|
||||
// Setting the hierarchy: Material -> Properties70 -> P -> Properties
|
||||
FBXNode properties70Node;
|
||||
properties70Node.name = PROPERTIES70_NODE_NAME;
|
||||
|
||||
// Set diffuseColor
|
||||
FBXNode pNodeDiffuseColor;
|
||||
{
|
||||
pNodeDiffuseColor.name = P_NODE_NAME;
|
||||
pNodeDiffuseColor.properties.append({
|
||||
"DiffuseColor", "Color", "", "A",
|
||||
currentMaterial.diffuseColor[0], currentMaterial.diffuseColor[1], currentMaterial.diffuseColor[2]
|
||||
});
|
||||
}
|
||||
properties70Node.children.append(pNodeDiffuseColor);
|
||||
|
||||
// Set specularColor
|
||||
FBXNode pNodeSpecularColor;
|
||||
{
|
||||
pNodeSpecularColor.name = P_NODE_NAME;
|
||||
pNodeSpecularColor.properties.append({
|
||||
"SpecularColor", "Color", "", "A",
|
||||
currentMaterial.specularColor[0], currentMaterial.specularColor[1], currentMaterial.specularColor[2]
|
||||
});
|
||||
}
|
||||
properties70Node.children.append(pNodeSpecularColor);
|
||||
|
||||
// Set Shininess
|
||||
FBXNode pNodeShininess;
|
||||
{
|
||||
pNodeShininess.name = P_NODE_NAME;
|
||||
pNodeShininess.properties.append({
|
||||
"Shininess", "Number", "", "A",
|
||||
currentMaterial.shininess
|
||||
});
|
||||
}
|
||||
properties70Node.children.append(pNodeShininess);
|
||||
|
||||
// Set Opacity
|
||||
FBXNode pNodeOpacity;
|
||||
{
|
||||
pNodeOpacity.name = P_NODE_NAME;
|
||||
pNodeOpacity.properties.append({
|
||||
"Opacity", "Number", "", "A",
|
||||
currentMaterial.opacity
|
||||
});
|
||||
}
|
||||
properties70Node.children.append(pNodeOpacity);
|
||||
|
||||
materialNode.children.append(properties70Node);
|
||||
}
|
54
libraries/baking/src/OBJBaker.h
Normal file
54
libraries/baking/src/OBJBaker.h
Normal file
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// OBJBaker.h
|
||||
// libraries/baking/src
|
||||
//
|
||||
// Created by Utkarsh Gautam on 9/29/17.
|
||||
// Copyright 2017 High Fidelity, Inc.
|
||||
//
|
||||
// Distributed under the Apache License, Version 2.0.
|
||||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#ifndef hifi_OBJBaker_h
|
||||
#define hifi_OBJBaker_h
|
||||
|
||||
#include "Baker.h"
|
||||
#include "TextureBaker.h"
|
||||
#include "ModelBaker.h"
|
||||
|
||||
#include "ModelBakingLoggingCategory.h"
|
||||
|
||||
using TextureBakerThreadGetter = std::function<QThread*()>;
|
||||
|
||||
using NodeID = qlonglong;
|
||||
|
||||
class OBJBaker : public ModelBaker {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using ModelBaker::ModelBaker;
|
||||
|
||||
public slots:
|
||||
virtual void bake() override;
|
||||
|
||||
signals:
|
||||
void OBJLoaded();
|
||||
|
||||
private slots:
|
||||
void bakeOBJ();
|
||||
void handleOBJNetworkReply();
|
||||
|
||||
private:
|
||||
void loadOBJ();
|
||||
void createFBXNodeTree(FBXNode& rootNode, FBXGeometry& geometry);
|
||||
void setMaterialNodeProperties(FBXNode& materialNode, QString material, FBXGeometry& geometry);
|
||||
NodeID nextNodeID() { return _nodeID++; }
|
||||
|
||||
NodeID _nodeID { 0 };
|
||||
NodeID _geometryID;
|
||||
NodeID _modelID;
|
||||
std::vector<NodeID> _materialIDs;
|
||||
NodeID _textureID;
|
||||
std::vector<std::pair<NodeID, int>> _mapTextureMaterial;
|
||||
FBXNode _objectNode;
|
||||
};
|
||||
#endif // hifi_OBJBaker_h
|
|
@ -161,23 +161,19 @@ void FBXWriter::encodeFBXProperty(QDataStream& out, const QVariant& prop) {
|
|||
case QMetaType::QString:
|
||||
{
|
||||
auto bytes = prop.toString().toUtf8();
|
||||
out << 'S';
|
||||
out << bytes.length();
|
||||
out << bytes;
|
||||
out.device()->write("S", 1);
|
||||
out << (int32_t)bytes.size();
|
||||
out.writeRawData(bytes, bytes.size());
|
||||
break;
|
||||
}
|
||||
|
||||
case QMetaType::QByteArray:
|
||||
{
|
||||
auto bytes = prop.toByteArray();
|
||||
out.device()->write("S", 1);
|
||||
out << (int32_t)bytes.size();
|
||||
out.writeRawData(bytes, bytes.size());
|
||||
break;
|
||||
}
|
||||
|
||||
{
|
||||
auto bytes = prop.toByteArray();
|
||||
out.device()->write("S", 1);
|
||||
out << (int32_t)bytes.size();
|
||||
out.writeRawData(bytes, bytes.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
if (prop.canConvert<QVector<float>>()) {
|
||||
|
|
|
@ -643,13 +643,13 @@ done:
|
|||
}
|
||||
|
||||
|
||||
FBXGeometry* OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url) {
|
||||
FBXGeometry::Pointer OBJReader::readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url) {
|
||||
PROFILE_RANGE_EX(resource_parse, __FUNCTION__, 0xffff0000, nullptr);
|
||||
QBuffer buffer { &model };
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
FBXGeometry* geometryPtr = new FBXGeometry();
|
||||
FBXGeometry& geometry = *geometryPtr;
|
||||
auto geometryPtr { std::make_shared<FBXGeometry>() };
|
||||
FBXGeometry& geometry { *geometryPtr };
|
||||
OBJTokenizer tokenizer { &buffer };
|
||||
float scaleGuess = 1.0f;
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ public:
|
|||
QString currentMaterialName;
|
||||
QHash<QString, OBJMaterial> materials;
|
||||
|
||||
FBXGeometry* readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl());
|
||||
FBXGeometry::Pointer readOBJ(QByteArray& model, const QVariantHash& mapping, bool combineParts, const QUrl& url = QUrl());
|
||||
|
||||
private:
|
||||
QUrl _url;
|
||||
|
|
|
@ -20,9 +20,10 @@
|
|||
InputPluginList getInputPlugins() {
|
||||
InputPlugin* PLUGIN_POOL[] = {
|
||||
new KeyboardMouseDevice(),
|
||||
new TouchscreenDevice(),
|
||||
#if defined(Q_OS_ANDROID)
|
||||
new TouchscreenVirtualPadDevice(),
|
||||
#else
|
||||
new TouchscreenDevice(), // Touchscreen and Controller Scripts take care on Android
|
||||
#endif
|
||||
nullptr
|
||||
};
|
||||
|
|
|
@ -60,6 +60,8 @@ void TouchscreenVirtualPadDevice::init() {
|
|||
if (_fixedPosition) {
|
||||
virtualPadManager.getLeftVirtualPad()->setShown(virtualPadManager.isEnabled() && !virtualPadManager.isHidden()); // Show whenever it's enabled
|
||||
}
|
||||
|
||||
KeyboardMouseDevice::enableTouch(false); // Touch for view controls is managed by this plugin
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::setupFixedCenter(VirtualPad::Manager& virtualPadManager, bool force) {
|
||||
|
@ -73,8 +75,8 @@ void TouchscreenVirtualPadDevice::setupFixedCenter(VirtualPad::Manager& virtualP
|
|||
QScreen* eventScreen = qApp->primaryScreen(); // do not call every time
|
||||
_fixedCenterPosition = glm::vec2( _fixedRadius + margin, eventScreen->size().height() - margin - _fixedRadius - _extraBottomMargin);
|
||||
|
||||
_firstTouchLeftPoint = _fixedCenterPosition;
|
||||
virtualPadManager.getLeftVirtualPad()->setFirstTouch(_firstTouchLeftPoint);
|
||||
_moveRefTouchPoint = _fixedCenterPosition;
|
||||
virtualPadManager.getLeftVirtualPad()->setFirstTouch(_moveRefTouchPoint);
|
||||
}
|
||||
|
||||
float clip(float n, float lower, float upper) {
|
||||
|
@ -116,42 +118,35 @@ glm::vec2 TouchscreenVirtualPadDevice::clippedPointInCircle(float radius, glm::v
|
|||
return vec2(finalX, finalY);
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::processInputUseCircleMethod(VirtualPad::Manager& virtualPadManager) {
|
||||
vec2 clippedPoint = clippedPointInCircle(_fixedRadiusForCalc, _firstTouchLeftPoint, _currentTouchLeftPoint);
|
||||
void TouchscreenVirtualPadDevice::processInputDeviceForMove(VirtualPad::Manager& virtualPadManager) {
|
||||
vec2 clippedPoint = clippedPointInCircle(_fixedRadiusForCalc, _moveRefTouchPoint, _moveCurrentTouchPoint);
|
||||
|
||||
_inputDevice->_axisStateMap[controller::LX] = (clippedPoint.x - _firstTouchLeftPoint.x) / _fixedRadiusForCalc;
|
||||
_inputDevice->_axisStateMap[controller::LY] = (clippedPoint.y - _firstTouchLeftPoint.y) / _fixedRadiusForCalc;
|
||||
_inputDevice->_axisStateMap[controller::LX] = (clippedPoint.x - _moveRefTouchPoint.x) / _fixedRadiusForCalc;
|
||||
_inputDevice->_axisStateMap[controller::LY] = (clippedPoint.y - _moveRefTouchPoint.y) / _fixedRadiusForCalc;
|
||||
|
||||
virtualPadManager.getLeftVirtualPad()->setFirstTouch(_firstTouchLeftPoint);
|
||||
virtualPadManager.getLeftVirtualPad()->setFirstTouch(_moveRefTouchPoint);
|
||||
virtualPadManager.getLeftVirtualPad()->setCurrentTouch(clippedPoint);
|
||||
virtualPadManager.getLeftVirtualPad()->setBeingTouched(true);
|
||||
virtualPadManager.getLeftVirtualPad()->setShown(true); // If touched, show in any mode (fixed joystick position or non-fixed)
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::processInputUseSquareMethod(VirtualPad::Manager& virtualPadManager) {
|
||||
float leftDistanceScaleX, leftDistanceScaleY;
|
||||
leftDistanceScaleX = (_currentTouchLeftPoint.x - _firstTouchLeftPoint.x) / _screenDPIScale.x;
|
||||
leftDistanceScaleY = (_currentTouchLeftPoint.y - _firstTouchLeftPoint.y) / _screenDPIScale.y;
|
||||
void TouchscreenVirtualPadDevice::processInputDeviceForView() {
|
||||
float rightDistanceScaleX, rightDistanceScaleY;
|
||||
rightDistanceScaleX = (_viewCurrentTouchPoint.x - _viewRefTouchPoint.x) / _screenDPIScale.x;
|
||||
rightDistanceScaleY = (_viewCurrentTouchPoint.y - _viewRefTouchPoint.y) / _screenDPIScale.y;
|
||||
|
||||
leftDistanceScaleX = clip(leftDistanceScaleX, -STICK_RADIUS_INCHES, STICK_RADIUS_INCHES);
|
||||
leftDistanceScaleY = clip(leftDistanceScaleY, -STICK_RADIUS_INCHES, STICK_RADIUS_INCHES);
|
||||
rightDistanceScaleX = clip(rightDistanceScaleX, -_viewStickRadiusInches, _viewStickRadiusInches);
|
||||
rightDistanceScaleY = clip(rightDistanceScaleY, -_viewStickRadiusInches, _viewStickRadiusInches);
|
||||
|
||||
// NOW BETWEEN -1 1
|
||||
leftDistanceScaleX /= STICK_RADIUS_INCHES;
|
||||
leftDistanceScaleY /= STICK_RADIUS_INCHES;
|
||||
rightDistanceScaleX /= _viewStickRadiusInches;
|
||||
rightDistanceScaleY /= _viewStickRadiusInches;
|
||||
|
||||
_inputDevice->_axisStateMap[controller::LX] = leftDistanceScaleX;
|
||||
_inputDevice->_axisStateMap[controller::LY] = leftDistanceScaleY;
|
||||
_inputDevice->_axisStateMap[controller::RX] = rightDistanceScaleX;
|
||||
_inputDevice->_axisStateMap[controller::RY] = rightDistanceScaleY;
|
||||
|
||||
/* Shared variables for stick rendering (clipped to the stick radius)*/
|
||||
// Prevent this for being done when not in first person view
|
||||
virtualPadManager.getLeftVirtualPad()->setFirstTouch(_firstTouchLeftPoint);
|
||||
virtualPadManager.getLeftVirtualPad()->setCurrentTouch(
|
||||
glm::vec2(clip(_currentTouchLeftPoint.x, -STICK_RADIUS_INCHES * _screenDPIScale.x + _firstTouchLeftPoint.x, STICK_RADIUS_INCHES * _screenDPIScale.x + _firstTouchLeftPoint.x),
|
||||
clip(_currentTouchLeftPoint.y, -STICK_RADIUS_INCHES * _screenDPIScale.y + _firstTouchLeftPoint.y, STICK_RADIUS_INCHES * _screenDPIScale.y + _firstTouchLeftPoint.y))
|
||||
);
|
||||
virtualPadManager.getLeftVirtualPad()->setBeingTouched(true);
|
||||
virtualPadManager.getLeftVirtualPad()->setShown(true); // If touched, show in any mode (fixed joystick position or non-fixed)
|
||||
// after use, save last touch point as ref
|
||||
_viewRefTouchPoint = _viewCurrentTouchPoint;
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::pluginUpdate(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) {
|
||||
|
@ -163,8 +158,8 @@ void TouchscreenVirtualPadDevice::pluginUpdate(float deltaTime, const controller
|
|||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
setupFixedCenter(virtualPadManager);
|
||||
|
||||
if (_validTouchLeft) {
|
||||
processInputUseCircleMethod(virtualPadManager);
|
||||
if (_moveHasValidTouch) {
|
||||
processInputDeviceForMove(virtualPadManager);
|
||||
} else {
|
||||
virtualPadManager.getLeftVirtualPad()->setBeingTouched(false);
|
||||
if (_fixedPosition) {
|
||||
|
@ -175,20 +170,8 @@ void TouchscreenVirtualPadDevice::pluginUpdate(float deltaTime, const controller
|
|||
}
|
||||
}
|
||||
|
||||
if (_validTouchRight) {
|
||||
float rightDistanceScaleX, rightDistanceScaleY;
|
||||
rightDistanceScaleX = (_currentTouchRightPoint.x - _firstTouchRightPoint.x) / _screenDPIScale.x;
|
||||
rightDistanceScaleY = (_currentTouchRightPoint.y - _firstTouchRightPoint.y) / _screenDPIScale.y;
|
||||
|
||||
rightDistanceScaleX = clip(rightDistanceScaleX, -STICK_RADIUS_INCHES, STICK_RADIUS_INCHES);
|
||||
rightDistanceScaleY = clip(rightDistanceScaleY, -STICK_RADIUS_INCHES, STICK_RADIUS_INCHES);
|
||||
|
||||
// NOW BETWEEN -1 1
|
||||
rightDistanceScaleX /= STICK_RADIUS_INCHES;
|
||||
rightDistanceScaleY /= STICK_RADIUS_INCHES;
|
||||
|
||||
_inputDevice->_axisStateMap[controller::RX] = rightDistanceScaleX;
|
||||
_inputDevice->_axisStateMap[controller::RY] = rightDistanceScaleY;
|
||||
if (_viewHasValidTouch) {
|
||||
processInputDeviceForView();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -228,70 +211,133 @@ void TouchscreenVirtualPadDevice::touchBeginEvent(const QTouchEvent* event) {
|
|||
// touch begin here is a big begin -> begins both pads? maybe it does nothing
|
||||
debugPoints(event, " BEGIN ++++++++++++++++");
|
||||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
if (!virtualPadManager.isEnabled()) {
|
||||
if (!virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
|
||||
return;
|
||||
}
|
||||
KeyboardMouseDevice::enableTouch(false);
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchEndEvent(const QTouchEvent* event) {
|
||||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
if (!virtualPadManager.isEnabled()) {
|
||||
touchLeftEnd();
|
||||
touchRightEnd();
|
||||
if (!virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
|
||||
moveTouchEnd();
|
||||
viewTouchEnd();
|
||||
return;
|
||||
}
|
||||
// touch end here is a big reset -> resets both pads
|
||||
_touchPointCount = 0;
|
||||
KeyboardMouseDevice::enableTouch(true);
|
||||
_unusedTouches.clear();
|
||||
debugPoints(event, " END ----------------");
|
||||
touchLeftEnd();
|
||||
touchRightEnd();
|
||||
moveTouchEnd();
|
||||
viewTouchEnd();
|
||||
_inputDevice->_axisStateMap.clear();
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::processUnusedTouches(std::map<int, TouchType> unusedTouchesInEvent) {
|
||||
std::vector<int> touchesToDelete;
|
||||
for (auto const& touchEntry : _unusedTouches) {
|
||||
if (!unusedTouchesInEvent.count(touchEntry.first)) {
|
||||
touchesToDelete.push_back(touchEntry.first);
|
||||
}
|
||||
}
|
||||
for (int touchToDelete : touchesToDelete) {
|
||||
_unusedTouches.erase(touchToDelete);
|
||||
}
|
||||
|
||||
for (auto const& touchEntry : unusedTouchesInEvent) {
|
||||
if (!_unusedTouches.count(touchEntry.first)) {
|
||||
_unusedTouches[touchEntry.first] = touchEntry.second;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchUpdateEvent(const QTouchEvent* event) {
|
||||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
if (!virtualPadManager.isEnabled()) {
|
||||
touchLeftEnd();
|
||||
touchRightEnd();
|
||||
if (!virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
|
||||
moveTouchEnd();
|
||||
viewTouchEnd();
|
||||
return;
|
||||
}
|
||||
_touchPointCount = event->touchPoints().count();
|
||||
|
||||
const QList<QTouchEvent::TouchPoint>& tPoints = event->touchPoints();
|
||||
bool leftTouchFound = false;
|
||||
bool rightTouchFound = false;
|
||||
bool moveTouchFound = false;
|
||||
bool viewTouchFound = false;
|
||||
|
||||
int idxMoveStartingPointCandidate = -1;
|
||||
int idxViewStartingPointCandidate = -1;
|
||||
|
||||
glm::vec2 thisPoint;
|
||||
int thisPointId;
|
||||
std::map<int, TouchType> unusedTouchesInEvent;
|
||||
|
||||
for (int i = 0; i < _touchPointCount; ++i) {
|
||||
glm::vec2 thisPoint(tPoints[i].pos().x(), tPoints[i].pos().y());
|
||||
if (_validTouchLeft) {
|
||||
leftTouchFound = true;
|
||||
touchLeftUpdate(thisPoint);
|
||||
} else if (touchLeftBeginPointIsValid(thisPoint)) {
|
||||
if (!leftTouchFound) {
|
||||
leftTouchFound = true;
|
||||
touchLeftBegin(thisPoint);
|
||||
}
|
||||
thisPoint.x = tPoints[i].pos().x();
|
||||
thisPoint.y = tPoints[i].pos().y();
|
||||
thisPointId = tPoints[i].id();
|
||||
|
||||
if (!moveTouchFound && _moveHasValidTouch && _moveCurrentTouchId == thisPointId) {
|
||||
// valid if it's an ongoing touch
|
||||
moveTouchFound = true;
|
||||
moveTouchUpdate(thisPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!viewTouchFound && _viewHasValidTouch && _viewCurrentTouchId == thisPointId) {
|
||||
// valid if it's an ongoing touch
|
||||
viewTouchFound = true;
|
||||
viewTouchUpdate(thisPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!moveTouchFound && idxMoveStartingPointCandidate == -1 && moveTouchBeginIsValid(thisPoint) &&
|
||||
(!_unusedTouches.count(thisPointId) || _unusedTouches[thisPointId] == MOVE )) {
|
||||
idxMoveStartingPointCandidate = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!viewTouchFound && idxViewStartingPointCandidate == -1 && viewTouchBeginIsValid(thisPoint) &&
|
||||
(!_unusedTouches.count(thisPointId) || _unusedTouches[thisPointId] == VIEW )) {
|
||||
idxViewStartingPointCandidate = i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (moveTouchBeginIsValid(thisPoint)) {
|
||||
unusedTouchesInEvent[thisPointId] = MOVE;
|
||||
} else if (viewTouchBeginIsValid(thisPoint)) {
|
||||
unusedTouchesInEvent[thisPointId] = VIEW;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
processUnusedTouches(unusedTouchesInEvent);
|
||||
|
||||
if (!moveTouchFound) {
|
||||
if (idxMoveStartingPointCandidate != -1) {
|
||||
_moveCurrentTouchId = tPoints[idxMoveStartingPointCandidate].id();
|
||||
_unusedTouches.erase(_moveCurrentTouchId);
|
||||
moveTouchBegin(thisPoint);
|
||||
} else {
|
||||
if (!rightTouchFound) {
|
||||
rightTouchFound = true;
|
||||
if (!_validTouchRight) {
|
||||
touchRightBegin(thisPoint);
|
||||
} else {
|
||||
touchRightUpdate(thisPoint);
|
||||
}
|
||||
}
|
||||
moveTouchEnd();
|
||||
}
|
||||
}
|
||||
if (!leftTouchFound) {
|
||||
touchLeftEnd();
|
||||
}
|
||||
if (!rightTouchFound) {
|
||||
touchRightEnd();
|
||||
if (!viewTouchFound) {
|
||||
if (idxViewStartingPointCandidate != -1) {
|
||||
_viewCurrentTouchId = tPoints[idxViewStartingPointCandidate].id();
|
||||
_unusedTouches.erase(_viewCurrentTouchId);
|
||||
viewTouchBegin(thisPoint);
|
||||
} else {
|
||||
viewTouchEnd();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool TouchscreenVirtualPadDevice::touchLeftBeginPointIsValid(glm::vec2 touchPoint) {
|
||||
bool TouchscreenVirtualPadDevice::viewTouchBeginIsValid(glm::vec2 touchPoint) {
|
||||
return !moveTouchBeginIsValid(touchPoint);
|
||||
}
|
||||
|
||||
bool TouchscreenVirtualPadDevice::moveTouchBeginIsValid(glm::vec2 touchPoint) {
|
||||
if (_fixedPosition) {
|
||||
// inside circle
|
||||
return pow(touchPoint.x - _fixedCenterPosition.x,2.0) + pow(touchPoint.y - _fixedCenterPosition.y, 2.0) < pow(_fixedRadius, 2.0);
|
||||
|
@ -301,45 +347,46 @@ bool TouchscreenVirtualPadDevice::touchLeftBeginPointIsValid(glm::vec2 touchPoin
|
|||
}
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchLeftBegin(glm::vec2 touchPoint) {
|
||||
void TouchscreenVirtualPadDevice::moveTouchBegin(glm::vec2 touchPoint) {
|
||||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
if (virtualPadManager.isEnabled()) {
|
||||
if (virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
|
||||
if (_fixedPosition) {
|
||||
_firstTouchLeftPoint = _fixedCenterPosition;
|
||||
_moveRefTouchPoint = _fixedCenterPosition;
|
||||
} else {
|
||||
_firstTouchLeftPoint = touchPoint;
|
||||
_moveRefTouchPoint = touchPoint;
|
||||
}
|
||||
_validTouchLeft = true;
|
||||
_moveHasValidTouch = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchLeftUpdate(glm::vec2 touchPoint) {
|
||||
_currentTouchLeftPoint = touchPoint;
|
||||
void TouchscreenVirtualPadDevice::moveTouchUpdate(glm::vec2 touchPoint) {
|
||||
_moveCurrentTouchPoint = touchPoint;
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchLeftEnd() {
|
||||
if (_validTouchLeft) { // do stuff once
|
||||
_validTouchLeft = false;
|
||||
void TouchscreenVirtualPadDevice::moveTouchEnd() {
|
||||
if (_moveHasValidTouch) { // do stuff once
|
||||
_moveHasValidTouch = false;
|
||||
_inputDevice->_axisStateMap[controller::LX] = 0;
|
||||
_inputDevice->_axisStateMap[controller::LY] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchRightBegin(glm::vec2 touchPoint) {
|
||||
void TouchscreenVirtualPadDevice::viewTouchBegin(glm::vec2 touchPoint) {
|
||||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
if (virtualPadManager.isEnabled()) {
|
||||
_firstTouchRightPoint = touchPoint;
|
||||
_validTouchRight = true;
|
||||
if (virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
|
||||
_viewRefTouchPoint = touchPoint;
|
||||
_viewCurrentTouchPoint = touchPoint;
|
||||
_viewHasValidTouch = true;
|
||||
}
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchRightUpdate(glm::vec2 touchPoint) {
|
||||
_currentTouchRightPoint = touchPoint;
|
||||
void TouchscreenVirtualPadDevice::viewTouchUpdate(glm::vec2 touchPoint) {
|
||||
_viewCurrentTouchPoint = touchPoint;
|
||||
}
|
||||
|
||||
void TouchscreenVirtualPadDevice::touchRightEnd() {
|
||||
if (_validTouchRight) { // do stuff once
|
||||
_validTouchRight = false;
|
||||
void TouchscreenVirtualPadDevice::viewTouchEnd() {
|
||||
if (_viewHasValidTouch) { // do stuff once
|
||||
_viewHasValidTouch = false;
|
||||
_inputDevice->_axisStateMap[controller::RX] = 0;
|
||||
_inputDevice->_axisStateMap[controller::RY] = 0;
|
||||
}
|
||||
|
@ -347,7 +394,7 @@ void TouchscreenVirtualPadDevice::touchRightEnd() {
|
|||
|
||||
void TouchscreenVirtualPadDevice::touchGestureEvent(const QGestureEvent* event) {
|
||||
auto& virtualPadManager = VirtualPad::Manager::instance();
|
||||
if (!virtualPadManager.isEnabled()) {
|
||||
if (!virtualPadManager.isEnabled() && !virtualPadManager.isHidden()) {
|
||||
return;
|
||||
}
|
||||
if (QGesture* gesture = event->gesture(Qt::PinchGesture)) {
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
class QTouchEvent;
|
||||
class QGestureEvent;
|
||||
|
||||
const float STICK_RADIUS_INCHES = .3f;
|
||||
|
||||
class TouchscreenVirtualPadDevice : public InputPlugin {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
@ -62,17 +60,30 @@ public:
|
|||
const std::shared_ptr<InputDevice>& getInputDevice() const { return _inputDevice; }
|
||||
|
||||
protected:
|
||||
|
||||
enum TouchType {
|
||||
MOVE = 1,
|
||||
VIEW
|
||||
};
|
||||
|
||||
float _lastPinchScale;
|
||||
float _pinchScale;
|
||||
float _screenDPI;
|
||||
qreal _screenDPIProvided;
|
||||
glm::vec2 _screenDPIScale;
|
||||
bool _validTouchLeft;
|
||||
glm::vec2 _firstTouchLeftPoint;
|
||||
glm::vec2 _currentTouchLeftPoint;
|
||||
bool _validTouchRight;
|
||||
glm::vec2 _firstTouchRightPoint;
|
||||
glm::vec2 _currentTouchRightPoint;
|
||||
|
||||
bool _moveHasValidTouch;
|
||||
glm::vec2 _moveRefTouchPoint;
|
||||
glm::vec2 _moveCurrentTouchPoint;
|
||||
int _moveCurrentTouchId;
|
||||
|
||||
bool _viewHasValidTouch;
|
||||
glm::vec2 _viewRefTouchPoint;
|
||||
glm::vec2 _viewCurrentTouchPoint;
|
||||
int _viewCurrentTouchId;
|
||||
|
||||
std::map<int, TouchType> _unusedTouches;
|
||||
|
||||
int _touchPointCount;
|
||||
int _screenWidthCenter;
|
||||
std::shared_ptr<InputDevice> _inputDevice { std::make_shared<InputDevice>() };
|
||||
|
@ -83,18 +94,26 @@ protected:
|
|||
float _fixedRadiusForCalc;
|
||||
int _extraBottomMargin {0};
|
||||
|
||||
void touchLeftBegin(glm::vec2 touchPoint);
|
||||
void touchLeftUpdate(glm::vec2 touchPoint);
|
||||
void touchLeftEnd();
|
||||
bool touchLeftBeginPointIsValid(glm::vec2 touchPoint);
|
||||
void touchRightBegin(glm::vec2 touchPoint);
|
||||
void touchRightUpdate(glm::vec2 touchPoint);
|
||||
void touchRightEnd();
|
||||
float _viewStickRadiusInches {0.1333f}; // agreed default
|
||||
|
||||
void moveTouchBegin(glm::vec2 touchPoint);
|
||||
void moveTouchUpdate(glm::vec2 touchPoint);
|
||||
void moveTouchEnd();
|
||||
bool moveTouchBeginIsValid(glm::vec2 touchPoint);
|
||||
|
||||
void viewTouchBegin(glm::vec2 touchPoint);
|
||||
void viewTouchUpdate(glm::vec2 touchPoint);
|
||||
void viewTouchEnd();
|
||||
bool viewTouchBeginIsValid(glm::vec2 touchPoint);
|
||||
|
||||
void setupFixedCenter(VirtualPad::Manager& virtualPadManager, bool force = false);
|
||||
|
||||
void processInputUseCircleMethod(VirtualPad::Manager& virtualPadManager);
|
||||
void processInputUseSquareMethod(VirtualPad::Manager& virtualPadManager);
|
||||
void processInputDeviceForMove(VirtualPad::Manager& virtualPadManager);
|
||||
glm::vec2 clippedPointInCircle(float radius, glm::vec2 origin, glm::vec2 touchPoint);
|
||||
|
||||
void processUnusedTouches(std::map<int, TouchType> unusedTouchesInEvent);
|
||||
|
||||
void processInputDeviceForView();
|
||||
// just for debug
|
||||
private:
|
||||
void debugPoints(const QTouchEvent* event, QString who);
|
||||
|
|
|
@ -190,11 +190,11 @@ void GeometryReader::run() {
|
|||
throw QString("empty geometry, possibly due to an unsupported FBX version");
|
||||
}
|
||||
} else if (_url.path().toLower().endsWith(".obj")) {
|
||||
fbxGeometry.reset(OBJReader().readOBJ(_data, _mapping, _combineParts, _url));
|
||||
fbxGeometry = OBJReader().readOBJ(_data, _mapping, _combineParts, _url);
|
||||
} else if (_url.path().toLower().endsWith(".obj.gz")) {
|
||||
QByteArray uncompressedData;
|
||||
if (gunzip(_data, uncompressedData)) {
|
||||
fbxGeometry.reset(OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url));
|
||||
if (gunzip(_data, uncompressedData)){
|
||||
fbxGeometry = OBJReader().readOBJ(uncompressedData, _mapping, _combineParts, _url);
|
||||
} else {
|
||||
throw QString("failed to decompress .obj.gz");
|
||||
}
|
||||
|
|
|
@ -26,12 +26,14 @@
|
|||
#include "NetworkAccessManager.h"
|
||||
#include "NetworkLogging.h"
|
||||
|
||||
ResourceManager::ResourceManager() {
|
||||
ResourceManager::ResourceManager(bool atpSupportEnabled) : _atpSupportEnabled(atpSupportEnabled) {
|
||||
_thread.setObjectName("Resource Manager Thread");
|
||||
|
||||
auto assetClient = DependencyManager::set<AssetClient>();
|
||||
assetClient->moveToThread(&_thread);
|
||||
QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching);
|
||||
if (_atpSupportEnabled) {
|
||||
auto assetClient = DependencyManager::set<AssetClient>();
|
||||
assetClient->moveToThread(&_thread);
|
||||
QObject::connect(&_thread, &QThread::started, assetClient.data(), &AssetClient::initCaching);
|
||||
}
|
||||
|
||||
_thread.start();
|
||||
}
|
||||
|
@ -111,6 +113,10 @@ ResourceRequest* ResourceManager::createResourceRequest(QObject* parent, const Q
|
|||
} else if (scheme == URL_SCHEME_HTTP || scheme == URL_SCHEME_HTTPS || scheme == URL_SCHEME_FTP) {
|
||||
request = new HTTPResourceRequest(normalizedURL);
|
||||
} else if (scheme == URL_SCHEME_ATP) {
|
||||
if (!_atpSupportEnabled) {
|
||||
qCDebug(networking) << "ATP support not enabled, unable to create request for URL: " << url.url();
|
||||
return nullptr;
|
||||
}
|
||||
request = new AssetResourceRequest(normalizedURL);
|
||||
} else {
|
||||
qCDebug(networking) << "Unknown scheme (" << scheme << ") for URL: " << url.url();
|
||||
|
@ -146,7 +152,7 @@ bool ResourceManager::resourceExists(const QUrl& url) {
|
|||
reply->deleteLater();
|
||||
|
||||
return reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200;
|
||||
} else if (scheme == URL_SCHEME_ATP) {
|
||||
} else if (scheme == URL_SCHEME_ATP && _atpSupportEnabled) {
|
||||
auto request = new AssetResourceRequest(url);
|
||||
ByteRange range;
|
||||
range.fromInclusive = 1;
|
||||
|
|
|
@ -34,7 +34,7 @@ class ResourceManager: public QObject, public Dependency {
|
|||
SINGLETON_DEPENDENCY
|
||||
|
||||
public:
|
||||
ResourceManager();
|
||||
ResourceManager(bool atpSupportEnabled = true);
|
||||
|
||||
void setUrlPrefixOverride(const QString& prefix, const QString& replacement);
|
||||
QString normalizeURL(const QString& urlString);
|
||||
|
@ -57,6 +57,7 @@ private:
|
|||
|
||||
using PrefixMap = std::map<QString, QString>;
|
||||
|
||||
bool _atpSupportEnabled;
|
||||
PrefixMap _prefixMap;
|
||||
QMutex _prefixMapLock;
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
|||
App.openUrl("https://metaverse.highfidelity.com/marketplace?category=avatars");
|
||||
break;
|
||||
case 'hide':
|
||||
Controller.setVPadHidden(false);
|
||||
module.exports.onHidden();
|
||||
break;
|
||||
default:
|
||||
|
@ -114,26 +115,27 @@ module.exports = {
|
|||
qml: "hifi/avatarSelection.qml",
|
||||
visible: false
|
||||
});
|
||||
/*,
|
||||
visible: false*/
|
||||
if (window) {
|
||||
window.fromQml.connect(fromQml);
|
||||
}
|
||||
init();
|
||||
},
|
||||
show: function() {
|
||||
Controller.setVPadHidden(true);
|
||||
if (window) {
|
||||
window.setVisible(true);
|
||||
isVisible = true;
|
||||
}
|
||||
},
|
||||
hide: function() {
|
||||
Controller.setVPadHidden(false);
|
||||
if (window) {
|
||||
window.setVisible(false);
|
||||
}
|
||||
isVisible = false;
|
||||
},
|
||||
destroy: function() {
|
||||
Controller.setVPadHidden(false);
|
||||
if (window) {
|
||||
window.fromQml.disconnect(fromQml);
|
||||
window.close();
|
||||
|
@ -155,5 +157,7 @@ module.exports = {
|
|||
refreshSelectedAvatar: function(currentAvatarURL) {
|
||||
refreshSelected(currentAvatarURL);
|
||||
},
|
||||
onHidden: function() { }
|
||||
onHidden: function() {
|
||||
Controller.setVPadHidden(false);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "DomainBaker.h"
|
||||
|
||||
#include <QtConcurrent>
|
||||
#include <QtCore/QEventLoop>
|
||||
#include <QtCore/QFile>
|
||||
|
@ -17,10 +19,9 @@
|
|||
#include <QtCore/QJsonObject>
|
||||
|
||||
#include "Gzip.h"
|
||||
|
||||
#include "Oven.h"
|
||||
|
||||
#include "DomainBaker.h"
|
||||
#include "FBXBaker.h"
|
||||
#include "OBJBaker.h"
|
||||
|
||||
DomainBaker::DomainBaker(const QUrl& localModelFileURL, const QString& domainName,
|
||||
const QString& baseOutputPath, const QUrl& destinationPath,
|
||||
|
@ -167,15 +168,18 @@ void DomainBaker::enumerateEntities() {
|
|||
// check if the file pointed to by this URL is a bakeable model, by comparing extensions
|
||||
auto modelFileName = modelURL.fileName();
|
||||
|
||||
static const QString BAKEABLE_MODEL_EXTENSION { ".fbx" };
|
||||
static const QString BAKEABLE_MODEL_FBX_EXTENSION { ".fbx" };
|
||||
static const QString BAKEABLE_MODEL_OBJ_EXTENSION { ".obj" };
|
||||
static const QString BAKED_MODEL_EXTENSION = ".baked.fbx";
|
||||
|
||||
bool isBakedFBX = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive);
|
||||
bool isUnbakedFBX = modelFileName.endsWith(BAKEABLE_MODEL_EXTENSION, Qt::CaseInsensitive) && !isBakedFBX;
|
||||
bool isBakedModel = modelFileName.endsWith(BAKED_MODEL_EXTENSION, Qt::CaseInsensitive);
|
||||
bool isBakeableFBX = modelFileName.endsWith(BAKEABLE_MODEL_FBX_EXTENSION, Qt::CaseInsensitive);
|
||||
bool isBakeableOBJ = modelFileName.endsWith(BAKEABLE_MODEL_OBJ_EXTENSION, Qt::CaseInsensitive);
|
||||
bool isBakeable = isBakeableFBX || isBakeableOBJ;
|
||||
|
||||
if (isUnbakedFBX || (_shouldRebakeOriginals && isBakedFBX)) {
|
||||
if (isBakeable || (_shouldRebakeOriginals && isBakedModel)) {
|
||||
|
||||
if (isBakedFBX) {
|
||||
if (isBakedModel) {
|
||||
// grab a URL to the original, that we assume is stored a directory up, in the "original" folder
|
||||
// with just the fbx extension
|
||||
qDebug() << "Re-baking original for" << modelURL;
|
||||
|
@ -190,7 +194,7 @@ void DomainBaker::enumerateEntities() {
|
|||
modelURL = modelURL.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment);
|
||||
}
|
||||
|
||||
// setup an FBXBaker for this URL, as long as we don't already have one
|
||||
// setup a ModelBaker for this URL, as long as we don't already have one
|
||||
if (!_modelBakers.contains(modelURL)) {
|
||||
auto filename = modelURL.fileName();
|
||||
auto baseName = filename.left(filename.lastIndexOf('.'));
|
||||
|
@ -199,12 +203,23 @@ void DomainBaker::enumerateEntities() {
|
|||
while (QDir(_contentOutputPath + subDirName).exists()) {
|
||||
subDirName = "/" + baseName + "-" + QString::number(i++);
|
||||
}
|
||||
QSharedPointer<FBXBaker> baker {
|
||||
new FBXBaker(modelURL, []() -> QThread* {
|
||||
return Oven::instance().getNextWorkerThread();
|
||||
}, _contentOutputPath + subDirName + "/baked", _contentOutputPath + subDirName + "/original"),
|
||||
&FBXBaker::deleteLater
|
||||
};
|
||||
|
||||
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);
|
||||
|
@ -299,16 +314,16 @@ void DomainBaker::bakeSkybox(QUrl skyboxURL, QJsonValueRef entity) {
|
|||
}
|
||||
|
||||
void DomainBaker::handleFinishedModelBaker() {
|
||||
auto baker = qobject_cast<FBXBaker*>(sender());
|
||||
auto baker = qobject_cast<ModelBaker*>(sender());
|
||||
|
||||
if (baker) {
|
||||
if (!baker->hasErrors()) {
|
||||
// this FBXBaker is done and everything went according to plan
|
||||
qDebug() << "Re-writing entity references to" << baker->getFBXUrl();
|
||||
qDebug() << "Re-writing entity references to" << baker->getModelURL();
|
||||
|
||||
// enumerate the QJsonRef values for the URL of this FBX from our multi hash of
|
||||
// entity objects needing a URL re-write
|
||||
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getFBXUrl())) {
|
||||
for (QJsonValueRef entityValue : _entitiesNeedingRewrite.values(baker->getModelURL())) {
|
||||
|
||||
// convert the entity QJsonValueRef to a QJsonObject so we can modify its URL
|
||||
auto entity = entityValue.toObject();
|
||||
|
@ -317,7 +332,7 @@ void DomainBaker::handleFinishedModelBaker() {
|
|||
QUrl oldModelURL { entity[ENTITY_MODEL_URL_KEY].toString() };
|
||||
|
||||
// setup a new URL using the prefix we were passed
|
||||
auto relativeFBXFilePath = baker->getBakedFBXFilePath().remove(_contentOutputPath);
|
||||
auto relativeFBXFilePath = baker->getBakedModelFilePath().remove(_contentOutputPath);
|
||||
if (relativeFBXFilePath.startsWith("/")) {
|
||||
relativeFBXFilePath = relativeFBXFilePath.right(relativeFBXFilePath.length() - 1);
|
||||
}
|
||||
|
@ -370,10 +385,10 @@ void DomainBaker::handleFinishedModelBaker() {
|
|||
}
|
||||
|
||||
// remove the baked URL from the multi hash of entities needing a re-write
|
||||
_entitiesNeedingRewrite.remove(baker->getFBXUrl());
|
||||
_entitiesNeedingRewrite.remove(baker->getModelURL());
|
||||
|
||||
// drop our shared pointer to this baker so that it gets cleaned up
|
||||
_modelBakers.remove(baker->getFBXUrl());
|
||||
_modelBakers.remove(baker->getModelURL());
|
||||
|
||||
// emit progress to tell listeners how many models we have baked
|
||||
emit bakeProgress(++_completedSubBakes, _totalNumberOfSubBakes);
|
||||
|
|
|
@ -61,7 +61,7 @@ private:
|
|||
|
||||
QJsonArray _entities;
|
||||
|
||||
QHash<QUrl, QSharedPointer<FBXBaker>> _modelBakers;
|
||||
QHash<QUrl, QSharedPointer<ModelBaker>> _modelBakers;
|
||||
QHash<QUrl, QSharedPointer<TextureBaker>> _skyboxBakers;
|
||||
|
||||
QMultiHash<QUrl, QJsonValueRef> _entitiesNeedingRewrite;
|
||||
|
|
|
@ -9,12 +9,16 @@
|
|||
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
//
|
||||
|
||||
#include "Oven.h"
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include <image/Image.h>
|
||||
|
||||
#include "Oven.h"
|
||||
#include <DependencyManager.h>
|
||||
#include <StatTracker.h>
|
||||
#include <ResourceManager.h>
|
||||
|
||||
Oven* Oven::_staticInstance { nullptr };
|
||||
|
||||
|
@ -29,6 +33,10 @@ Oven::Oven() {
|
|||
|
||||
// setup our worker threads
|
||||
setupWorkerThreads(QThread::idealThreadCount());
|
||||
|
||||
// Initialize dependencies for OBJ Baker
|
||||
DependencyManager::set<StatTracker>();
|
||||
DependencyManager::set<ResourceManager>(false);
|
||||
}
|
||||
|
||||
Oven::~Oven() {
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class QThread;
|
||||
|
||||
class Oven {
|
||||
|
||||
|
@ -31,7 +34,7 @@ private:
|
|||
|
||||
std::vector<std::unique_ptr<QThread>> _workerThreads;
|
||||
|
||||
std::atomic<uint> _nextWorkerThreadIndex;
|
||||
std::atomic<uint32_t> _nextWorkerThreadIndex;
|
||||
int _numWorkerThreads;
|
||||
|
||||
static Oven* _staticInstance;
|
||||
|
|
|
@ -41,5 +41,5 @@ void BakeWidget::cancelButtonClicked() {
|
|||
auto stackedWidget = qobject_cast<QStackedWidget*>(parentWidget());
|
||||
stackedWidget->removeWidget(this);
|
||||
|
||||
this->deleteLater();
|
||||
deleteLater();
|
||||
}
|
||||
|
|
|
@ -21,18 +21,21 @@
|
|||
#include <QtCore/QDebug>
|
||||
#include <QtCore/QThread>
|
||||
|
||||
#include "../Oven.h"
|
||||
#include "../OvenGUIApplication.h"
|
||||
|
||||
#include "OvenMainWindow.h"
|
||||
#include "FBXBaker.h"
|
||||
#include "OBJBaker.h"
|
||||
#include "ModelBakeWidget.h"
|
||||
|
||||
|
||||
static const auto EXPORT_DIR_SETTING_KEY = "model_export_directory";
|
||||
static const auto MODEL_START_DIR_SETTING_KEY = "model_search_directory";
|
||||
|
||||
ModelBakeWidget::ModelBakeWidget(QWidget* parent, Qt::WindowFlags flags) :
|
||||
BakeWidget(parent, flags),
|
||||
_exportDirectory(EXPORT_DIR_SETTING_KEY),
|
||||
_modelStartDirectory(MODEL_START_DIR_SETTING_KEY)
|
||||
{
|
||||
_modelStartDirectory(MODEL_START_DIR_SETTING_KEY) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
|
@ -113,7 +116,7 @@ void ModelBakeWidget::chooseFileButtonClicked() {
|
|||
startDir = QDir::homePath();
|
||||
}
|
||||
|
||||
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx)");
|
||||
auto selectedFiles = QFileDialog::getOpenFileNames(this, "Choose Model", startDir, "Models (*.fbx *.obj)");
|
||||
|
||||
if (!selectedFiles.isEmpty()) {
|
||||
// set the contents of the model file text box to be the path to the selected file
|
||||
|
@ -189,7 +192,7 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
subFolderName = modelName + "-" + QString::number(++iteration) + "/";
|
||||
}
|
||||
|
||||
outputDirectory.mkdir(subFolderName);
|
||||
outputDirectory.mkpath(subFolderName);
|
||||
|
||||
if (!outputDirectory.exists()) {
|
||||
QMessageBox::warning(this, "Unable to create directory", "Unable to create output directory. Please create it manually or choose a different directory.");
|
||||
|
@ -200,16 +203,25 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
|
||||
QDir bakedOutputDirectory = outputDirectory.absoluteFilePath("baked");
|
||||
QDir originalOutputDirectory = outputDirectory.absoluteFilePath("original");
|
||||
|
||||
|
||||
bakedOutputDirectory.mkdir(".");
|
||||
originalOutputDirectory.mkdir(".");
|
||||
|
||||
// everything seems to be in place, kick off a bake for this model now
|
||||
auto baker = std::unique_ptr<FBXBaker> {
|
||||
new FBXBaker(modelToBakeURL, []() -> QThread* {
|
||||
return Oven::instance().getNextWorkerThread();
|
||||
}, bakedOutputDirectory.absolutePath(), originalOutputDirectory.absolutePath())
|
||||
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());
|
||||
|
@ -218,7 +230,7 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
QMetaObject::invokeMethod(baker.get(), "bake");
|
||||
|
||||
// make sure we hear about the results of this baker when it is done
|
||||
connect(baker.get(), &FBXBaker::finished, this, &ModelBakeWidget::handleFinishedBaker);
|
||||
connect(baker.get(), &Baker::finished, this, &ModelBakeWidget::handleFinishedBaker);
|
||||
|
||||
// add a pending row to the results window to show that this bake is in process
|
||||
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
|
||||
|
@ -231,27 +243,31 @@ void ModelBakeWidget::bakeButtonClicked() {
|
|||
}
|
||||
|
||||
void ModelBakeWidget::handleFinishedBaker() {
|
||||
if (auto baker = qobject_cast<FBXBaker*>(sender())) {
|
||||
// add the results of this bake to the results window
|
||||
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
|
||||
return value.first.get() == baker;
|
||||
});
|
||||
Baker* baker = dynamic_cast<Baker*>(sender());
|
||||
if (!baker) {
|
||||
qWarning() << "Received signal from unexpected sender";
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& file : baker->getOutputFiles()) {
|
||||
qDebug() << "Baked file: " << file;
|
||||
// add the results of this bake to the results window
|
||||
auto it = std::find_if(_bakers.begin(), _bakers.end(), [baker](const BakerRowPair& value) {
|
||||
return value.first.get() == baker;
|
||||
});
|
||||
|
||||
for (auto& file : baker->getOutputFiles()) {
|
||||
qDebug() << "Baked file: " << file;
|
||||
}
|
||||
|
||||
if (it != _bakers.end()) {
|
||||
auto resultRow = it->second;
|
||||
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
|
||||
|
||||
if (baker->hasErrors()) {
|
||||
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
|
||||
} else {
|
||||
resultsWindow->changeStatusForRow(resultRow, "Success");
|
||||
}
|
||||
|
||||
if (it != _bakers.end()) {
|
||||
auto resultRow = it->second;
|
||||
auto resultsWindow = OvenGUIApplication::instance()->getMainWindow()->showResultsWindow();
|
||||
|
||||
if (baker->hasErrors()) {
|
||||
resultsWindow->changeStatusForRow(resultRow, baker->getErrors().join("\n"));
|
||||
} else {
|
||||
resultsWindow->changeStatusForRow(resultRow, "Success");
|
||||
}
|
||||
|
||||
_bakers.erase(it);
|
||||
}
|
||||
_bakers.erase(it);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
|
||||
#include <SettingHandle.h>
|
||||
|
||||
#include <FBXBaker.h>
|
||||
|
||||
#include "BakeWidget.h"
|
||||
|
||||
class QLineEdit;
|
||||
|
|
|
@ -46,7 +46,7 @@ ResultsWindow* OvenMainWindow::showResultsWindow(bool shouldRaise) {
|
|||
_resultsWindow->show();
|
||||
|
||||
// place the results window initially below our window
|
||||
_resultsWindow->move(_resultsWindow->x(), this->frameGeometry().bottom());
|
||||
_resultsWindow->move(_resultsWindow->x(), frameGeometry().bottom());
|
||||
}
|
||||
|
||||
// show the results window and make sure it is in front
|
||||
|
|
|
@ -41,18 +41,17 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) {
|
|||
}
|
||||
try {
|
||||
QByteArray fbxContents = fbx.readAll();
|
||||
FBXGeometry* geom;
|
||||
FBXGeometry::Pointer geom;
|
||||
if (filename.toLower().endsWith(".obj")) {
|
||||
bool combineParts = false;
|
||||
geom = OBJReader().readOBJ(fbxContents, QVariantHash(), combineParts);
|
||||
} else if (filename.toLower().endsWith(".fbx")) {
|
||||
geom = readFBX(fbxContents, QVariantHash(), filename);
|
||||
geom.reset(readFBX(fbxContents, QVariantHash(), filename));
|
||||
} else {
|
||||
qWarning() << "file has unknown extension" << filename;
|
||||
return false;
|
||||
}
|
||||
result = *geom;
|
||||
delete geom;
|
||||
|
||||
reSortFBXGeometryMeshes(result);
|
||||
} catch (const QString& error) {
|
||||
|
|
Loading…
Reference in a new issue