diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 0deb241343..ebfa9b54f0 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -15,7 +15,12 @@ #include #include +#include +#include +#include +#include // .obj files are not locale-specific. The C/ASCII charset applies. +#include #include "FBXReader.h" #include "OBJReader.h" #include "Shape.h" @@ -25,35 +30,14 @@ QHash COMMENT_SCALE_HINTS = {{"This file uses centimeters as units", 1.0f / 100.0f}, {"This file uses millimeters as units", 1.0f / 1000.0f}}; - -class OBJTokenizer { -public: - OBJTokenizer(QIODevice* device); - enum SpecialToken { - NO_TOKEN = -1, - NO_PUSHBACKED_TOKEN = -1, - DATUM_TOKEN = 0x100, - COMMENT_TOKEN = 0x101 - }; - int nextToken(); - const QByteArray& getDatum() const { return _datum; } - bool isNextTokenFloat(); - void skipLine() { _device->readLine(); } - void pushBackToken(int token) { _pushedBackToken = token; } - void ungetChar(char ch) { _device->ungetChar(ch); } - const QString getComment() const { return _comment; } - -private: - QIODevice* _device; - QByteArray _datum; - int _pushedBackToken; - QString _comment; -}; - +const QString SMART_DEFAULT_MATERIAL_NAME = "High Fidelity smart default material name"; OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } +const QByteArray OBJTokenizer::getLineAsDatum() { + return _device->readLine().trimmed(); +} int OBJTokenizer::nextToken() { if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { @@ -116,14 +100,35 @@ bool OBJTokenizer::isNextTokenFloat() { return ok; } -void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) { +glm::vec3 OBJTokenizer::getVec3() { + auto x = getFloat(); // N.B.: getFloat() has side-effect + auto y = getFloat(); // And order of arguments is different on Windows/Linux. + auto z = getFloat(); + auto v = glm::vec3(x, y, z); + while (isNextTokenFloat()) { + // the spec(s) get(s) vague here. might be w, might be a color... chop it off. + nextToken(); + } + return v; +} +glm::vec2 OBJTokenizer::getVec2() { + auto v = glm::vec2(getFloat(), 1.0f - getFloat()); // OBJ has an odd sense of u, v. Also N.B.: getFloat() has side-effect + while (isNextTokenFloat()) { + // there can be a w, but we don't handle that + nextToken(); + } + return v; +} + + +void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { meshPart.diffuseColor = glm::vec3(1, 1, 1); meshPart.specularColor = glm::vec3(1, 1, 1); meshPart.emissiveColor = glm::vec3(0, 0, 0); meshPart.emissiveParams = glm::vec2(0, 1); meshPart.shininess = 40; meshPart.opacity = 1; - + meshPart.materialID = materialID; meshPart.opacity = 1.0; meshPart._material = model::MaterialPointer(new model::Material()); @@ -134,14 +139,146 @@ void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) { meshPart._material->setEmissive(glm::vec3(0.0, 0.0, 0.0)); } -bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, - FBXGeometry &geometry, QVector& faceNormals, QVector& faceNormalIndexes, - float& scaleGuess) { - FBXMesh &mesh = geometry.meshes[0]; +// OBJFace +bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices) { + bool ok; + int index = vertexIndex.toInt(&ok); + if (!ok) { + return false; + } + vertexIndices.append(index - 1); + if (!textureIndex.isEmpty()) { + index = textureIndex.toInt(&ok); + if (!ok) { + return false; + } + if (index < 0) { // Count backwards from the last one added. + index = vertices.count() + 1 + index; + } + textureUVIndices.append(index - 1); + } + if (!normalIndex.isEmpty()) { + index = normalIndex.toInt(&ok); + if (!ok) { + return false; + } + normalIndices.append(index - 1); + } + return true; +} +QVector OBJFace::triangulate() { + QVector newFaces; + const int nVerticesInATriangle = 3; + if (vertexIndices.count() == nVerticesInATriangle) { + newFaces.append(*this); + } else { + for (int i = 1; i < vertexIndices.count() - 1; i++) { + OBJFace newFace; + newFace.addFrom(this, 0); + newFace.addFrom(this, i); + newFace.addFrom(this, i + 1); + newFace.groupName = groupName; + newFace.materialName = materialName; + newFaces.append(newFace); + } + } + return newFaces; +} +void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f at index i + vertexIndices.append(face->vertexIndices[index]); + if (face->textureUVIndices.count() > 0) { // Any at all. Runtime error if not consistent. + textureUVIndices.append(face->textureUVIndices[index]); + } + if (face->normalIndices.count() > 0) { + normalIndices.append(face->normalIndices[index]); + } +} + +bool OBJReader::isValidTexture(const QByteArray &filename) { + QUrl candidateUrl = url->resolved(QUrl(filename)); + QNetworkReply *netReply = request(candidateUrl, true); + bool isValid = netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200); + netReply->deleteLater(); + return isValid; +} + +void OBJReader::parseMaterialLibrary(QIODevice* device) { + OBJTokenizer tokenizer(device); + QString matName = SMART_DEFAULT_MATERIAL_NAME; + OBJMaterial& currentMaterial = materials[matName]; + while (true) { + switch (tokenizer.nextToken()) { + case OBJTokenizer::COMMENT_TOKEN: + qCDebug(modelformat) << "OBJ Reader MTLLIB comment:" << tokenizer.getComment(); + break; + case OBJTokenizer::DATUM_TOKEN: + break; + default: + materials[matName] = currentMaterial; + qCDebug(modelformat) << "OBJ Reader Last material shininess:" << currentMaterial.shininess << " opacity:" << currentMaterial.opacity << " diffuse color:" << currentMaterial.diffuseColor << " specular color:" << currentMaterial.specularColor << " diffuse texture:" << currentMaterial.diffuseTextureFilename << " specular texture:" << currentMaterial.specularTextureFilename; + return; + } + QByteArray token = tokenizer.getDatum(); + if (token == "newmtl") { + if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { + return; + } + materials[matName] = currentMaterial; + matName = tokenizer.getDatum(); + currentMaterial = materials[matName]; + currentMaterial.diffuseTextureFilename = "test"; + qCDebug(modelformat) << "OBJ Reader Starting new material definition " << matName; + currentMaterial.diffuseTextureFilename = ""; + } else if (token == "Ns") { + currentMaterial.shininess = tokenizer.getFloat(); + } else if ((token == "d") || (token == "Tr")) { + currentMaterial.opacity = tokenizer.getFloat(); + } else if (token == "Ka") { + qCDebug(modelformat) << "OBJ Reader Ignoring material Ka " << tokenizer.getVec3(); + } else if (token == "Kd") { + currentMaterial.diffuseColor = tokenizer.getVec3(); + } else if (token == "Ks") { + currentMaterial.specularColor = tokenizer.getVec3(); + } else if ((token == "map_Kd") || (token == "map_Ks")) { + QByteArray filename = QUrl(tokenizer.getLineAsDatum()).fileName().toUtf8(); + if (filename.endsWith(".tga")) { + qCDebug(modelformat) << "OBJ Reader WARNING: currently ignoring tga texture " << filename << " in " << url; + break; + } + if (isValidTexture(filename)) { + if (token == "map_Kd") { + currentMaterial.diffuseTextureFilename = filename; + } else { + currentMaterial.specularTextureFilename = filename; + } + } else { + qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " ignoring missing texture " << filename; + } + } + } +} + +QNetworkReply* OBJReader::request(QUrl& url, bool isTest) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest netRequest(url); + QNetworkReply* netReply = isTest ? networkAccessManager.head(netRequest) : networkAccessManager.get(netRequest); + QEventLoop loop; // Create an event loop that will quit when we get the finished signal + QObject::connect(netReply, SIGNAL(finished()), &loop, SLOT(quit())); + loop.exec(); // Nothing is going to happen on this whole run thread until we get this + netReply->waitForReadyRead(-1); // so we might as well block this thread waiting for the response, rather than + return netReply; // trying to sync later on. +} + + +bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess) { + FaceGroup faces; + FBXMesh& mesh = geometry.meshes[0]; mesh.parts.append(FBXMeshPart()); - FBXMeshPart &meshPart = mesh.parts.last(); + FBXMeshPart& meshPart = mesh.parts.last(); bool sawG = false; bool result = true; + int originalFaceCountForDebugging = 0; + QString currentGroup; setMeshPartDefaults(meshPart, QString("dontknow") + QString::number(mesh.parts.count())); @@ -165,6 +302,7 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, break; } QByteArray token = tokenizer.getDatum(); + //qCDebug(modelformat) << token; if (token == "g") { if (sawG) { // we've encountered the beginning of the next group. @@ -176,54 +314,43 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, break; } QByteArray groupName = tokenizer.getDatum(); - meshPart.materialID = groupName; + currentGroup = groupName; + //qCDebug(modelformat) << "new group:" << groupName; + } else if (token == "mtllib") { + if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { + break; + } + QByteArray libraryName = tokenizer.getDatum(); + if (librariesSeen.contains(libraryName)) { + break; // Some files use mtllib over and over again for the same libraryName + } + librariesSeen[libraryName] = true; + QUrl libraryUrl = url->resolved(QUrl(libraryName).fileName()); // Throw away any path part of libraryName, and merge against original url. + qCDebug(modelformat) << "OBJ Reader new library:" << libraryName << " at:" << libraryUrl; + QNetworkReply* netReply = request(libraryUrl, false); + if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) { + parseMaterialLibrary(netReply); + } else { + qCDebug(modelformat) << "OBJ Reader " << libraryName << " did not answer. Got " << netReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + } + netReply->deleteLater(); + } else if (token == "usemtl") { + if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { + break; + } + currentMaterialName = tokenizer.getDatum(); + qCDebug(modelformat) << "OBJ Reader new current material:" << currentMaterialName; } else if (token == "v") { - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - break; - } - float x = std::stof(tokenizer.getDatum().data()); - - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - break; - } - float y = std::stof(tokenizer.getDatum().data()); - - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - break; - } - float z = std::stof(tokenizer.getDatum().data()); - - while (tokenizer.isNextTokenFloat()) { - // the spec(s) get(s) vague here. might be w, might be a color... chop it off. - tokenizer.nextToken(); - } - mesh.vertices.append(glm::vec3(x, y, z)); + vertices.append(tokenizer.getVec3()); } else if (token == "vn") { - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - break; - } - float x = std::stof(tokenizer.getDatum().data()); - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - break; - } - float y = std::stof(tokenizer.getDatum().data()); - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - break; - } - float z = std::stof(tokenizer.getDatum().data()); - - while (tokenizer.isNextTokenFloat()) { - // the spec gets vague here. might be w - tokenizer.nextToken(); - } - faceNormals.append(glm::vec3(x, y, z)); + normals.append(tokenizer.getVec3()); + } else if (token == "vt") { + textureUVs.append(tokenizer.getVec2()); } else if (token == "f") { - // a face can have 3 or more vertices - QVector indices; - QVector normalIndices; + OBJFace face; while (true) { if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - if (indices.count() == 0) { + if (face.vertexIndices.count() == 0) { // nonsense, bail out. goto done; } @@ -233,69 +360,22 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, // vertex-index // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index - QByteArray token = tokenizer.getDatum(); - QList parts = token.split('/'); - assert(parts.count() >= 1); - assert(parts.count() <= 3); - QByteArray vertIndexBA = parts[ 0 ]; - - bool ok; - int vertexIndex = vertIndexBA.toInt(&ok); - if (!ok) { - // it wasn't #/#/#, put it back and exit this loop. + if (!isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } - - // if (parts.count() > 1) { - // QByteArray textureIndexBA = parts[ 1 ]; - // } - - if (parts.count() > 2) { - QByteArray normalIndexBA = parts[ 2 ]; - bool ok; - int normalIndex = normalIndexBA.toInt(&ok); - if (ok) { - normalIndices.append(normalIndex - 1); - } - } - - // negative indexes count backward from the current end of the vertex list - vertexIndex = (vertexIndex >= 0 ? vertexIndex : mesh.vertices.count() + vertexIndex + 1); - // obj index is 1 based - assert(vertexIndex >= 1); - indices.append(vertexIndex - 1); + QList parts = token.split('/'); + assert(parts.count() >= 1); + assert(parts.count() <= 3); + const QByteArray noData {}; + face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices); + face.groupName = currentGroup; + face.materialName = currentMaterialName; } - - if (indices.count() == 3) { - meshPart.triangleIndices.append(indices[0]); - meshPart.triangleIndices.append(indices[1]); - meshPart.triangleIndices.append(indices[2]); - if (normalIndices.count() == 3) { - faceNormalIndexes.append(normalIndices[0]); - faceNormalIndexes.append(normalIndices[1]); - faceNormalIndexes.append(normalIndices[2]); - } else { - // hmm. - } - } else if (indices.count() == 4) { - meshPart.quadIndices << indices; - } else { - // some obj writers (maya) will write a face with lots of points. - for (int i = 1; i < indices.count() - 1; i++) { - // break the face into triangles - meshPart.triangleIndices.append(indices[0]); - meshPart.triangleIndices.append(indices[i]); - meshPart.triangleIndices.append(indices[i+1]); - } - if (indices.count() == normalIndices.count()) { - for (int i = 1; i < normalIndices.count() - 1; i++) { - faceNormalIndexes.append(normalIndices[0]); - faceNormalIndexes.append(normalIndices[i]); - faceNormalIndexes.append(normalIndices[i+1]); - } - } + originalFaceCountForDebugging++; + foreach(OBJFace face, face.triangulate()) { + faces.append(face); } } else { // something we don't (yet) care about @@ -303,61 +383,40 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, tokenizer.skipLine(); } } - - done: - - if (meshPart.triangleIndices.size() == 0 && meshPart.quadIndices.size() == 0) { - // empty mesh? +done: + if (faces.count() == 0) { // empty mesh mesh.parts.pop_back(); } - + faceGroups.append(faces); // We're done with this group. Add the faces. + //qCDebug(modelformat) << "end group:" << meshPart.materialID << " original faces:" << originalFaceCountForDebugging << " triangles:" << faces.count() << " keep going:" << result; return result; } -FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping) { +FBXGeometry OBJReader::readOBJ(const QByteArray& model, const QVariantHash& mapping) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); - return readOBJ(&buffer, mapping); + return readOBJ(&buffer, mapping, nullptr); } -FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping) { +FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, QUrl* url) { FBXGeometry geometry; OBJTokenizer tokenizer(device); - QVector faceNormalIndexes; - QVector faceNormals; float scaleGuess = 1.0f; - faceNormalIndexes.clear(); - + this->url = url; geometry.meshExtents.reset(); geometry.meshes.append(FBXMesh()); - + try { // call parseOBJGroup as long as it's returning true. Each successful call will // add a new meshPart to the geometry's single mesh. - bool success = true; - while (success) { - success = parseOBJGroup(tokenizer, mapping, geometry, faceNormals, faceNormalIndexes, scaleGuess); - } + while (parseOBJGroup(tokenizer, mapping, geometry, scaleGuess)) {} - FBXMesh &mesh = geometry.meshes[0]; + FBXMesh& mesh = geometry.meshes[0]; mesh.meshIndex = 0; - - // if we got a hint about units, scale all the points - if (scaleGuess != 1.0f) { - for (int i = 0; i < mesh.vertices.size(); i++) { - mesh.vertices[i] *= scaleGuess; - } - } - - mesh.meshExtents.reset(); - foreach (const glm::vec3& vertex, mesh.vertices) { - mesh.meshExtents.addPoint(vertex); - geometry.meshExtents.addPoint(vertex); - } - + geometry.joints.resize(1); geometry.joints[0].isFree = false; geometry.joints[0].parentIndex = -1; @@ -380,63 +439,96 @@ FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping) { 0, 0, 1, 0, 0, 0, 0, 1); mesh.clusters.append(cluster); - - // The OBJ format has normals for faces. The FBXGeometry structure has normals for points. - // run through all the faces, look-up (or determine) a normal and set the normal for the points - // that make up each face. - QVector pointNormalsSums; - - mesh.normals.fill(glm::vec3(0,0,0), mesh.vertices.count()); - pointNormalsSums.fill(glm::vec3(0,0,0), mesh.vertices.count()); - - foreach (FBXMeshPart meshPart, mesh.parts) { - int triCount = meshPart.triangleIndices.count() / 3; - for (int i = 0; i < triCount; i++) { - int p0Index = meshPart.triangleIndices[i*3]; - int p1Index = meshPart.triangleIndices[i*3+1]; - int p2Index = meshPart.triangleIndices[i*3+2]; - - assert(p0Index < mesh.vertices.count()); - assert(p1Index < mesh.vertices.count()); - assert(p2Index < mesh.vertices.count()); - - glm::vec3 n0, n1, n2; - if (i < faceNormalIndexes.count()) { - int n0Index = faceNormalIndexes[i*3]; - int n1Index = faceNormalIndexes[i*3+1]; - int n2Index = faceNormalIndexes[i*3+2]; - n0 = faceNormals[n0Index]; - n1 = faceNormals[n1Index]; - n2 = faceNormals[n2Index]; - } else { - // We didn't read normals, add bogus normal data for this face - glm::vec3 p0 = mesh.vertices[p0Index]; - glm::vec3 p1 = mesh.vertices[p1Index]; - glm::vec3 p2 = mesh.vertices[p2Index]; - n0 = glm::cross(p1 - p0, p2 - p0); - n1 = n0; - n2 = n0; - } - - // we sum up the normal for each point and then divide by the count to get an average - pointNormalsSums[p0Index] += n0; - pointNormalsSums[p1Index] += n1; - pointNormalsSums[p2Index] += n2; + + // Some .obj files use the convention that a group with uv coordinates that doesn't define a material, should use a texture with the same basename as the .obj file. + QString filename = url->fileName(); + int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail + QString basename = filename.remove(extIndex + 1, sizeof("obj")); + OBJMaterial& preDefinedMaterial = materials[SMART_DEFAULT_MATERIAL_NAME]; + preDefinedMaterial.diffuseColor = glm::vec3(1.0f); + QVector extensions = {"jpg", "jpeg", "png", "tga"}; + QByteArray base = basename.toUtf8(), textName = ""; + for (int i = 0; i < extensions.count(); i++) { + QByteArray candidateString = base + extensions[i]; + if (isValidTexture(candidateString)) { + textName = candidateString; + break; } - - int vertCount = mesh.vertices.count(); - for (int i = 0; i < vertCount; i++) { - float length = glm::length(pointNormalsSums[i]); - if (length > FLT_EPSILON) { - mesh.normals[i] = glm::normalize(pointNormalsSums[i]); - } - } - - // XXX do same normal calculation for quadCount } - } - catch(const std::exception& e) { - qCDebug(modelformat) << "something went wrong in OBJ reader"; + if (!textName.isEmpty()) { + preDefinedMaterial.diffuseTextureFilename = textName; + } + materials[SMART_DEFAULT_MATERIAL_NAME] = preDefinedMaterial; + + for (int i = 0, meshPartCount = 0; i < mesh.parts.count(); i++, meshPartCount++) { + FBXMeshPart& meshPart = mesh.parts[i]; + FaceGroup faceGroup = faceGroups[meshPartCount]; + OBJFace leadFace = faceGroup[0]; // All the faces in the same group will have the same name and material. + QString groupMaterialName = leadFace.materialName; + if (groupMaterialName.isEmpty() && (leadFace.textureUVIndices.count() > 0)) { + qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " needs a texture that isn't specified. Using default mechanism."; + groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; + } else if (!groupMaterialName.isEmpty() && !materials.contains(groupMaterialName)) { + qCDebug(modelformat) << "OBJ Reader WARNING: " << url << " specifies a material " << groupMaterialName << " that is not defined. Using default mechanism."; + groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; + } + if (!groupMaterialName.isEmpty()) { + OBJMaterial* material = &materials[groupMaterialName]; + // The code behind this is in transition. Some things are set directly in the FXBMeshPart... + meshPart.materialID = groupMaterialName; + meshPart.diffuseTexture.filename = material->diffuseTextureFilename; + meshPart.specularTexture.filename = material->specularTextureFilename; + // ... and some things are set in the underlying material. + meshPart._material->setDiffuse(material->diffuseColor); + meshPart._material->setSpecular(material->specularColor); + meshPart._material->setShininess(material->shininess); + meshPart._material->setOpacity(material->opacity); + } + qCDebug(modelformat) << "OBJ Reader part:" << meshPartCount << "name:" << leadFace.groupName << "material:" << groupMaterialName << "diffuse:" << meshPart._material->getDiffuse() << "faces:" << faceGroup.count() << "triangle indices will start with:" << mesh.vertices.count(); + foreach(OBJFace face, faceGroup) { + glm::vec3 v0 = vertices[face.vertexIndices[0]]; + glm::vec3 v1 = vertices[face.vertexIndices[1]]; + glm::vec3 v2 = vertices[face.vertexIndices[2]]; + meshPart.triangleIndices.append(mesh.vertices.count()); // not face.vertexIndices into vertices + mesh.vertices << v0; + meshPart.triangleIndices.append(mesh.vertices.count()); + mesh.vertices << v1; + meshPart.triangleIndices.append(mesh.vertices.count()); + mesh.vertices << v2; + + glm::vec3 n0, n1, n2; + if (face.normalIndices.count()) { + n0 = normals[face.normalIndices[0]]; + n1 = normals[face.normalIndices[1]]; + n2 = normals[face.normalIndices[2]]; + } else { // generate normals from triangle plane if not provided + n0 = n1 = n2 = glm::cross(v1 - v0, v2 - v0); + } + mesh.normals << n0 << n1 << n2; + if (face.textureUVIndices.count()) { + mesh.texCoords + << textureUVs[face.textureUVIndices[0]] + << textureUVs[face.textureUVIndices[1]] + << textureUVs[face.textureUVIndices[2]]; + } + } + } + + // if we got a hint about units, scale all the points + if (scaleGuess != 1.0f) { + for (int i = 0; i < mesh.vertices.size(); i++) { + mesh.vertices[i] *= scaleGuess; + } + } + + mesh.meshExtents.reset(); + foreach (const glm::vec3& vertex, mesh.vertices) { + mesh.meshExtents.addPoint(vertex); + geometry.meshExtents.addPoint(vertex); + } + fbxDebugDump(geometry); + } catch(const std::exception& e) { + qCDebug(modelformat) << "OBJ reader fail: " << e.what(); } return geometry; @@ -453,11 +545,11 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { foreach (FBXMesh mesh, fbxgeo.meshes) { qCDebug(modelformat) << " vertices.count() =" << mesh.vertices.count(); qCDebug(modelformat) << " normals.count() =" << mesh.normals.count(); - if (mesh.normals.count() == mesh.vertices.count()) { + /*if (mesh.normals.count() == mesh.vertices.count()) { for (int i = 0; i < mesh.normals.count(); i++) { qCDebug(modelformat) << " " << mesh.vertices[ i ] << mesh.normals[ i ]; } - } + }*/ qCDebug(modelformat) << " tangents.count() =" << mesh.tangents.count(); qCDebug(modelformat) << " colors.count() =" << mesh.colors.count(); qCDebug(modelformat) << " texCoords.count() =" << mesh.texCoords.count(); @@ -470,13 +562,15 @@ void fbxDebugDump(const FBXGeometry& fbxgeo) { foreach (FBXMeshPart meshPart, mesh.parts) { qCDebug(modelformat) << " quadIndices.count() =" << meshPart.quadIndices.count(); qCDebug(modelformat) << " triangleIndices.count() =" << meshPart.triangleIndices.count(); - qCDebug(modelformat) << " diffuseColor =" << meshPart.diffuseColor; - qCDebug(modelformat) << " specularColor =" << meshPart.specularColor; - qCDebug(modelformat) << " emissiveColor =" << meshPart.emissiveColor; + qCDebug(modelformat) << " diffuseColor =" << meshPart.diffuseColor << "mat =" << meshPart._material->getDiffuse(); + qCDebug(modelformat) << " specularColor =" << meshPart.specularColor << "mat =" << meshPart._material->getSpecular(); + qCDebug(modelformat) << " emissiveColor =" << meshPart.emissiveColor << "mat =" << meshPart._material->getEmissive(); qCDebug(modelformat) << " emissiveParams =" << meshPart.emissiveParams; - qCDebug(modelformat) << " shininess =" << meshPart.shininess; - qCDebug(modelformat) << " opacity =" << meshPart.opacity; + qCDebug(modelformat) << " shininess =" << meshPart.shininess << "mat =" << meshPart._material->getShininess(); + qCDebug(modelformat) << " opacity =" << meshPart.opacity << "mat =" << meshPart._material->getOpacity(); qCDebug(modelformat) << " materialID =" << meshPart.materialID; + qCDebug(modelformat) << " diffuse texture =" << meshPart.diffuseTexture.filename; + qCDebug(modelformat) << " specular texture =" << meshPart.specularTexture.filename; } qCDebug(modelformat) << " clusters.count() =" << mesh.clusters.count(); foreach (FBXCluster cluster, mesh.clusters) { diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index a272e46f2d..a61665cb86 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -1,8 +1,86 @@ - +#include #include "FBXReader.h" -FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping); -FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping); -void fbxDebugDump(const FBXGeometry& fbxgeo); -void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID); +class OBJTokenizer { +public: + OBJTokenizer(QIODevice* device); + enum SpecialToken { + NO_TOKEN = -1, + NO_PUSHBACKED_TOKEN = -1, + DATUM_TOKEN = 0x100, + COMMENT_TOKEN = 0x101 + }; + int nextToken(); + const QByteArray& getDatum() const { return _datum; } + bool isNextTokenFloat(); + const QByteArray getLineAsDatum(); // some "filenames" have spaces in them + void skipLine() { _device->readLine(); } + void pushBackToken(int token) { _pushedBackToken = token; } + void ungetChar(char ch) { _device->ungetChar(ch); } + const QString getComment() const { return _comment; } + glm::vec3 getVec3(); + glm::vec2 getVec2(); + float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } + +private: + QIODevice* _device; + QByteArray _datum; + int _pushedBackToken; + QString _comment; +}; + +class OBJFace { // A single face, with three or more planar vertices. But see triangulate(). +public: + QVector vertexIndices; + QVector textureUVIndices; + QVector normalIndices; + QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. + QString materialName; + // Add one more set of vertex data. Answers true if successful + bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices); + // Return a set of one or more OBJFaces from this one, in which each is just a triangle. + // Even though FBXMeshPart can handle quads, it would be messy to try to keep track of mixed-size faces, so we treat everything as triangles. + QVector triangulate(); +private: + void addFrom(const OBJFace* face, int index); +}; + +// Materials and references to material names can come in any order, and different mesh parts can refer to the same material. +// Therefore it would get pretty hacky to try to use FBXMeshPart to store these as we traverse the files. +class OBJMaterial { +public: + float shininess; + float opacity; + glm::vec3 diffuseColor; + glm::vec3 specularColor; + QByteArray diffuseTextureFilename; + QByteArray specularTextureFilename; + OBJMaterial() : opacity(1.0f) {} +}; + +class OBJReader: public QObject { // QObject so we can make network requests. + Q_OBJECT +public: + typedef QVector FaceGroup; + QVector vertices; // all that we ever encounter while reading + QVector textureUVs; + QVector normals; + QVector faceGroups; + QString currentMaterialName; + QHash materials; + QUrl* url; + + QNetworkReply* request(QUrl& url, bool isTest); + FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping); + FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping, QUrl* url); +private: + QHash librariesSeen; + bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess); + void parseMaterialLibrary(QIODevice* device); + bool isValidTexture(const QByteArray &filename); // true if the file exists. TODO?: check content-type header and that it is a supported format. +}; + +// What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility. +void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID); +void fbxDebugDump(const FBXGeometry& fbxgeo); \ No newline at end of file diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 142fd2543b..d9dc0d2afa 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1797,8 +1797,8 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer(), -1, 0.0f, 0.0f, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), - glm::quat(), glm::mat4(), glm::mat4(), glm::vec3(), glm::vec3(), glm::quat(), glm::quat(), - glm::mat4(), QString(""), glm::vec3(), glm::quat(), SHAPE_TYPE_NONE, false}; + glm::quat(), glm::mat4(), glm::mat4(), glm::vec3(), glm::vec3(), glm::quat(), glm::quat(), + glm::mat4(), QString(""), glm::vec3(), glm::quat(), SHAPE_TYPE_NONE, false}; _geometry.joints.append(joint); _geometry.leftEyeJointIndex = -1; _geometry.rightEyeJointIndex = -1; @@ -2107,7 +2107,7 @@ void GeometryReader::run() { } fbxgeo = readFBX(_reply, _mapping, grabLightmaps, lightmapLevel); } else if (_url.path().toLower().endsWith(".obj")) { - fbxgeo = readOBJ(_reply, _mapping); + fbxgeo = OBJReader().readOBJ(_reply, _mapping, &_url); } QMetaObject::invokeMethod(geometry.data(), "setGeometry", Q_ARG(const FBXGeometry&, fbxgeo)); } else { diff --git a/tools/vhacd-util/src/VHACDUtil.cpp b/tools/vhacd-util/src/VHACDUtil.cpp index 92ae62db13..c24d6dc8f2 100644 --- a/tools/vhacd-util/src/VHACDUtil.cpp +++ b/tools/vhacd-util/src/VHACDUtil.cpp @@ -36,7 +36,7 @@ bool vhacd::VHACDUtil::loadFBX(const QString filename, FBXGeometry& result) { QByteArray fbxContents = fbx.readAll(); if (filename.toLower().endsWith(".obj")) { - result = readOBJ(fbxContents, QVariantHash()); + result = OBJReader().readOBJ(fbxContents, QVariantHash()); } else if (filename.toLower().endsWith(".fbx")) { result = readFBX(fbxContents, QVariantHash()); } else {