From bd5272d6770a48767c0806c3b2223bdf7f8ef2e2 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 13:35:21 -0700 Subject: [PATCH 01/18] Textures for .obj reader: Refactor so that uv coordinates can be associated with vertices that might otherwise be on different materials. Defer the assignment vertex/normal/uv assignment until later so that it could potentially be moved directly to model geometry instead of fbx geometry. Handle the actual uv coordinates. This version does not handle explicit .mtl files, but it does handle those .obj files that follow the convention that there is a .jpg with the same name as the .obj file. --- libraries/fbx/src/OBJReader.cpp | 351 ++++++++----------- libraries/fbx/src/OBJReader.h | 58 ++- libraries/render-utils/src/GeometryCache.cpp | 25 +- 3 files changed, 213 insertions(+), 221 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 0deb241343..28f1e2f1ce 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -26,35 +26,9 @@ QHash COMMENT_SCALE_HINTS = {{"This file uses centimeters as uni {"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; -}; - - OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } - int OBJTokenizer::nextToken() { if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { int token = _pushedBackToken; @@ -116,6 +90,24 @@ bool OBJTokenizer::isNextTokenFloat() { return ok; } +glm::vec3 OBJTokenizer::getVec3() { + auto v = glm::vec3(this->getFloat(), this->getFloat(), this->getFloat()); + while (this->isNextTokenFloat()) { + // the spec(s) get(s) vague here. might be w, might be a color... chop it off. + this->nextToken(); + } + return v; +} +glm::vec2 OBJTokenizer::getVec2() { + auto v = glm::vec2(this->getFloat(), 1.0f - this->getFloat()); // OBJ has an odd sense of u, v + while (this->isNextTokenFloat()) { + // there can be a w, but we don't handle that + this->nextToken(); + } + return v; +} + + void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) { meshPart.diffuseColor = glm::vec3(1, 1, 1); meshPart.specularColor = glm::vec3(1, 1, 1); @@ -123,7 +115,7 @@ void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) { 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 +126,57 @@ 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) { +// OBJFace +bool OBJFace::add(QByteArray vertexIndex, QByteArray textureIndex, QByteArray normalIndex) { + bool ok; + int index = vertexIndex.toInt(&ok); + if (!ok) { return false; } + vertexIndices.append(index - 1); + if (textureIndex != nullptr) { + index = textureIndex.toInt(&ok); + if (!ok) { return false; } + textureUVIndices.append(index - 1); + } + if (normalIndex != nullptr) { + index = normalIndex.toInt(&ok); + if (!ok) { return false; } + normalIndices.append(index - 1); + } + return true; +} +QVector OBJFace::triangulate() { + QVector newFaces; + if (vertexIndices.count() == 3) { + newFaces.append(*this); + } else { + for (int i = 1; i < vertexIndices.count() - 1; i++) { + OBJFace newFace; // FIXME: also copy materialName, groupName + newFace.addFrom(this, 0); + newFace.addFrom(this, i); + newFace.addFrom(this, i + 1); + newFaces.append(newFace); + } + } + return newFaces; +} +void OBJFace::addFrom(OBJFace const * f, int i) { // add using data from f at index i + vertexIndices.append(f->vertexIndices[i]); + if (f->textureUVIndices.count() > 0) { // Any at all. Runtime error if not consistent. + textureUVIndices.append(f->textureUVIndices[i]); + } + if (f->normalIndices.count() > 0) { + normalIndices.append(f->normalIndices[i]); + } +} + +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(); bool sawG = false; bool result = true; + int originalFaceCountForDebugging = 0; setMeshPartDefaults(meshPart, QString("dontknow") + QString::number(mesh.parts.count())); @@ -165,6 +200,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. @@ -177,53 +213,18 @@ bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, } QByteArray groupName = tokenizer.getDatum(); meshPart.materialID = groupName; + //qCDebug(modelformat) << "new group:" << groupName; } 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,131 +234,61 @@ 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 (!std::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); + // FIXME: if we want to handle negative indices, it has to be done here. + face.add(parts[0], (parts.count() > 1) ? parts[1] : nullptr, (parts.count() > 2) ? parts[2] : nullptr); + // FIXME: preserve current name, material and such } - - 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 { + } else { // something we don't (yet) care about // qCDebug(modelformat) << "OBJ parser is skipping a line with" << token; 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 this->readOBJ(&buffer, mapping); } -FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping) { +FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { FBXGeometry geometry; OBJTokenizer tokenizer(device); - QVector faceNormalIndexes; - QVector faceNormals; float scaleGuess = 1.0f; - faceNormalIndexes.clear(); - 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]; 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 +311,57 @@ 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()); - + + int meshPartCount = 0; + for (int i = 0; i < mesh.parts.count(); i++) { + FBXMeshPart & meshPart = mesh.parts[i]; + //qCDebug(modelformat) << "part:" << meshPartCount << " faces:" << faceGroups[meshPartCount].count() << "triangle indices will start with:" << mesh.vertices.count(); + foreach(OBJFace face, faceGroups[meshPartCount]) { + 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 (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; + 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); } - - // 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; - } - - 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]); + mesh.normals << n0 << n1 << n2; + if (face.textureUVIndices.count()) { + mesh.texCoords + << textureUVs[face.textureUVIndices[0]] + << textureUVs[face.textureUVIndices[1]] + << textureUVs[face.textureUVIndices[2]]; } } - - // XXX do same normal calculation for quadCount + meshPartCount++; } + + // 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); + } + //this->fbxDebugDump(geometry); } catch(const std::exception& e) { - qCDebug(modelformat) << "something went wrong in OBJ reader"; + qCDebug(modelformat) << "something went wrong in OBJ reader: " << e.what(); } return geometry; @@ -444,7 +369,7 @@ FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping) { -void fbxDebugDump(const FBXGeometry& fbxgeo) { +void OBJReader::fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << "---------------- fbxGeometry ----------------"; qCDebug(modelformat) << " hasSkeletonJoints =" << fbxgeo.hasSkeletonJoints; qCDebug(modelformat) << " offset =" << fbxgeo.offset; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index a272e46f2d..a6ccbb1a2f 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -2,7 +2,57 @@ #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(); + 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(); + +private: + float getFloat() { return std::stof((this->nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : this->getDatum().data()); } + 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; + //materialName groupName // FIXME + // Add one more set of vertex data. Answers true if successful + bool add(QByteArray vertexIndex, QByteArray textureIndex = nullptr, QByteArray normalIndex = nullptr); + // 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(OBJFace const * f, int i); +}; + +class OBJReader { +public: + typedef QVector FaceGroup; + QVector vertices; // all that we ever encounter while reading + QVector textureUVs; + QVector normals; + QVector faceGroups; + FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping); + FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping); + void fbxDebugDump(const FBXGeometry& fbxgeo); + bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, FBXGeometry &geometry, float& scaleGuess); +}; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 9d71ec5cc2..4bc8e8a2f2 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1796,9 +1796,7 @@ 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}; + FBXJoint joint = { false, QVector(), -1 }; _geometry.joints.append(joint); _geometry.leftEyeJointIndex = -1; _geometry.rightEyeJointIndex = -1; @@ -2107,7 +2105,26 @@ 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); + FBXMesh & mesh = fbxgeo.meshes[0]; // only one, by construction + if (mesh.texCoords.count() > 0) { // If we have uv texture coordinates.... + // ... then ensure that every meshPart has a texture filename. + // For now, that's defined directly, using the popular .obj convention that + // the texture is the same as the model basename. Later, we'll extend that + // with separate material files, too. + QString filename = _url.fileName(); + int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail + QString basename = filename.remove(extIndex + 1, 3); + QByteArray defaultTexture = basename.toUtf8() + "jpg"; + //qCDebug(renderutils) << "basename for " << filename << " is " << basename << ", default:" << defaultTexture; + QVector & meshParts = mesh.parts; + for (int i = 0; i < meshParts.count(); i++) { + FBXMeshPart & meshPart = meshParts[i]; + if (meshPart.diffuseTexture.filename.count() == 0) { + meshPart.diffuseTexture.filename = defaultTexture; + } + } + } } QMetaObject::invokeMethod(geometry.data(), "setGeometry", Q_ARG(const FBXGeometry&, fbxgeo)); } else { From 5d729b98d6e3709b2d0269c92bd78acc11f1f8cb Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 14:05:30 -0700 Subject: [PATCH 02/18] coding standard: brace placement --- libraries/fbx/src/OBJReader.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 28f1e2f1ce..08dc57780a 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -130,16 +130,22 @@ void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) { bool OBJFace::add(QByteArray vertexIndex, QByteArray textureIndex, QByteArray normalIndex) { bool ok; int index = vertexIndex.toInt(&ok); - if (!ok) { return false; } + if (!ok) { + return false; + } vertexIndices.append(index - 1); if (textureIndex != nullptr) { index = textureIndex.toInt(&ok); - if (!ok) { return false; } + if (!ok) { + return false; + } textureUVIndices.append(index - 1); } if (normalIndex != nullptr) { index = normalIndex.toInt(&ok); - if (!ok) { return false; } + if (!ok) { + return false; + } normalIndices.append(index - 1); } return true; From 59ed63027bee18e0b4b79ffa38aa2231fab548f8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 14:22:31 -0700 Subject: [PATCH 03/18] coding standard: magic numbers --- libraries/render-utils/src/GeometryCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 4bc8e8a2f2..a7bf840d3c 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2114,7 +2114,7 @@ void GeometryReader::run() { // with separate material files, too. QString filename = _url.fileName(); int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail - QString basename = filename.remove(extIndex + 1, 3); + QString basename = filename.remove(extIndex + 1, sizeof("obj")); QByteArray defaultTexture = basename.toUtf8() + "jpg"; //qCDebug(renderutils) << "basename for " << filename << " is " << basename << ", default:" << defaultTexture; QVector & meshParts = mesh.parts; From 1255d4614062cc417b5d15c74bc2d054d0828576 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 15:19:40 -0700 Subject: [PATCH 04/18] coding standard: type formatting and variable names --- libraries/fbx/src/OBJReader.cpp | 24 ++++++++++---------- libraries/fbx/src/OBJReader.h | 4 ++-- libraries/render-utils/src/GeometryCache.cpp | 6 ++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 08dc57780a..c27e470033 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -108,7 +108,7 @@ glm::vec2 OBJTokenizer::getVec2() { } -void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID) { +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); @@ -165,21 +165,21 @@ QVector OBJFace::triangulate() { } return newFaces; } -void OBJFace::addFrom(OBJFace const * f, int i) { // add using data from f at index i - vertexIndices.append(f->vertexIndices[i]); - if (f->textureUVIndices.count() > 0) { // Any at all. Runtime error if not consistent. - textureUVIndices.append(f->textureUVIndices[i]); +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 (f->normalIndices.count() > 0) { - normalIndices.append(f->normalIndices[i]); + if (face->normalIndices.count() > 0) { + normalIndices.append(face->normalIndices[index]); } } -bool OBJReader::parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, FBXGeometry &geometry, float& scaleGuess) { +bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess) { FaceGroup faces; - FBXMesh &mesh = geometry.meshes[0]; + 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; @@ -292,7 +292,7 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { // add a new meshPart to the geometry's single mesh. while (parseOBJGroup(tokenizer, mapping, geometry, scaleGuess)) {} - FBXMesh &mesh = geometry.meshes[0]; + FBXMesh& mesh = geometry.meshes[0]; mesh.meshIndex = 0; geometry.joints.resize(1); @@ -320,7 +320,7 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { int meshPartCount = 0; for (int i = 0; i < mesh.parts.count(); i++) { - FBXMeshPart & meshPart = mesh.parts[i]; + FBXMeshPart& meshPart = mesh.parts[i]; //qCDebug(modelformat) << "part:" << meshPartCount << " faces:" << faceGroups[meshPartCount].count() << "triangle indices will start with:" << mesh.vertices.count(); foreach(OBJFace face, faceGroups[meshPartCount]) { glm::vec3 v0 = vertices[face.vertexIndices[0]]; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index a6ccbb1a2f..92feb574f0 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -41,7 +41,7 @@ public: // 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(OBJFace const * f, int i); + void addFrom(const OBJFace* face, int index); }; class OBJReader { @@ -54,5 +54,5 @@ public: FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping); FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping); void fbxDebugDump(const FBXGeometry& fbxgeo); - bool parseOBJGroup(OBJTokenizer &tokenizer, const QVariantHash& mapping, FBXGeometry &geometry, float& scaleGuess); + bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess); }; diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index a7bf840d3c..b5c60a7834 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2106,7 +2106,7 @@ void GeometryReader::run() { fbxgeo = readFBX(_reply, _mapping, grabLightmaps, lightmapLevel); } else if (_url.path().toLower().endsWith(".obj")) { fbxgeo = OBJReader().readOBJ(_reply, _mapping); - FBXMesh & mesh = fbxgeo.meshes[0]; // only one, by construction + FBXMesh& mesh = fbxgeo.meshes[0]; // only one, by construction if (mesh.texCoords.count() > 0) { // If we have uv texture coordinates.... // ... then ensure that every meshPart has a texture filename. // For now, that's defined directly, using the popular .obj convention that @@ -2117,9 +2117,9 @@ void GeometryReader::run() { QString basename = filename.remove(extIndex + 1, sizeof("obj")); QByteArray defaultTexture = basename.toUtf8() + "jpg"; //qCDebug(renderutils) << "basename for " << filename << " is " << basename << ", default:" << defaultTexture; - QVector & meshParts = mesh.parts; + QVector& meshParts = mesh.parts; for (int i = 0; i < meshParts.count(); i++) { - FBXMeshPart & meshPart = meshParts[i]; + FBXMeshPart& meshPart = meshParts[i]; if (meshPart.diffuseTexture.filename.count() == 0) { meshPart.diffuseTexture.filename = defaultTexture; } From 1e81caab6cc254a66652f2e3b4224e35b09d266d Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 15:37:39 -0700 Subject: [PATCH 05/18] Get rid of superflous this->. Add comments about side-effect of getFloat(). --- libraries/fbx/src/OBJReader.cpp | 16 ++++++++-------- libraries/fbx/src/OBJReader.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index c27e470033..f317f8d82e 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -91,18 +91,18 @@ bool OBJTokenizer::isNextTokenFloat() { } glm::vec3 OBJTokenizer::getVec3() { - auto v = glm::vec3(this->getFloat(), this->getFloat(), this->getFloat()); - while (this->isNextTokenFloat()) { + auto v = glm::vec3(getFloat(), getFloat(), getFloat()); // N.B.: getFloat() has side-effect + while (isNextTokenFloat()) { // the spec(s) get(s) vague here. might be w, might be a color... chop it off. - this->nextToken(); + nextToken(); } return v; } glm::vec2 OBJTokenizer::getVec2() { - auto v = glm::vec2(this->getFloat(), 1.0f - this->getFloat()); // OBJ has an odd sense of u, v - while (this->isNextTokenFloat()) { + 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 - this->nextToken(); + nextToken(); } return v; } @@ -275,7 +275,7 @@ done: FBXGeometry OBJReader::readOBJ(const QByteArray& model, const QVariantHash& mapping) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); - return this->readOBJ(&buffer, mapping); + return readOBJ(&buffer, mapping); } @@ -364,7 +364,7 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { mesh.meshExtents.addPoint(vertex); geometry.meshExtents.addPoint(vertex); } - //this->fbxDebugDump(geometry); + //fbxDebugDump(geometry); } catch(const std::exception& e) { qCDebug(modelformat) << "something went wrong in OBJ reader: " << e.what(); diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 92feb574f0..9bc931086a 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -22,7 +22,7 @@ public: glm::vec2 getVec2(); private: - float getFloat() { return std::stof((this->nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : this->getDatum().data()); } + float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } QIODevice* _device; QByteArray _datum; int _pushedBackToken; From 3c88146d175a998b7dca7859120aceaccd406a6a Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 15:39:30 -0700 Subject: [PATCH 06/18] Fix indentation. --- libraries/fbx/src/OBJReader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index f317f8d82e..e51962e187 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -256,7 +256,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi foreach(OBJFace face, face.triangulate()) { faces.append(face); } - } else { + } else { // something we don't (yet) care about // qCDebug(modelformat) << "OBJ parser is skipping a line with" << token; tokenizer.skipLine(); From fb825f6d1000606909ecb4e9758d9140cb7530d1 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 15:45:38 -0700 Subject: [PATCH 07/18] Noisy constant for number of vertices in a triangle. --- libraries/fbx/src/OBJReader.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index e51962e187..d7eb74dfbb 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -152,7 +152,8 @@ bool OBJFace::add(QByteArray vertexIndex, QByteArray textureIndex, QByteArray no } QVector OBJFace::triangulate() { QVector newFaces; - if (vertexIndices.count() == 3) { + const int nVerticesInATriangle = 3; + if (vertexIndices.count() == nVerticesInATriangle) { newFaces.append(*this); } else { for (int i = 1; i < vertexIndices.count() - 1; i++) { From 810242e182b1238d963d3883b8875b2c33745bca Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 16:12:22 -0700 Subject: [PATCH 08/18] coding standard: comparison for pointer types (replaced with object isEmpty) --- libraries/fbx/src/OBJReader.cpp | 9 +++++---- libraries/fbx/src/OBJReader.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index d7eb74dfbb..e2ae739dbe 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -127,21 +127,21 @@ void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { } // OBJFace -bool OBJFace::add(QByteArray vertexIndex, QByteArray textureIndex, QByteArray normalIndex) { +bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { return false; } vertexIndices.append(index - 1); - if (textureIndex != nullptr) { + if (!textureIndex.isEmpty()) { index = textureIndex.toInt(&ok); if (!ok) { return false; } textureUVIndices.append(index - 1); } - if (normalIndex != nullptr) { + if (!normalIndex.isEmpty()) { index = normalIndex.toInt(&ok); if (!ok) { return false; @@ -250,7 +250,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi assert(parts.count() >= 1); assert(parts.count() <= 3); // FIXME: if we want to handle negative indices, it has to be done here. - face.add(parts[0], (parts.count() > 1) ? parts[1] : nullptr, (parts.count() > 2) ? parts[2] : nullptr); + const QByteArray noData {}; + face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData); // FIXME: preserve current name, material and such } originalFaceCountForDebugging++; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 9bc931086a..6a798c9a50 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -36,7 +36,7 @@ public: QVector normalIndices; //materialName groupName // FIXME // Add one more set of vertex data. Answers true if successful - bool add(QByteArray vertexIndex, QByteArray textureIndex = nullptr, QByteArray normalIndex = nullptr); + bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex); // 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(); From 242001a589e726a523d3f17944a72f93b754ffbd Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 22:17:44 -0700 Subject: [PATCH 09/18] Restore some very odd modularity used by VHACDUtil. --- libraries/fbx/src/OBJReader.h | 4 ++++ tools/vhacd-util/src/VHACDUtil.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 6a798c9a50..1734518493 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -56,3 +56,7 @@ public: void fbxDebugDump(const FBXGeometry& fbxgeo); bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess); }; + +// What are these utilities doing here? Apparently used by fbx loading code in VHACD Utils. +void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID); +void fbxDebugDump(const FBXGeometry& fbxgeo); \ No newline at end of file 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 { From 7245ca4ff3c7484fa6866f9edbbc2f1d4eb5cb44 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 29 Apr 2015 22:57:41 -0700 Subject: [PATCH 10/18] According to the jenkins build report, Windows/MSVC has a different definition of std C++ than ISO does. Fine. Make our own cross-platform isdigit. --- libraries/fbx/src/OBJReader.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index e2ae739dbe..3421b3c366 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -176,6 +176,12 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } +// Hmmm. Build report for Windows (which I don't have) reports that MSVC thinks isdigit isn't part of std lib. +// Fine. I'll roll my own. +bool fakeIsDigit(int character) { + return ('0' <= character) && (character <= '9'); +} + bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess) { FaceGroup faces; FBXMesh& mesh = geometry.meshes[0]; @@ -242,7 +248,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index QByteArray token = tokenizer.getDatum(); - if (!std::isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; + if (!fakeIsDigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } From fd63cb7ed1dfd44a437267b44038b2e4569112e3 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 30 Apr 2015 07:28:13 -0700 Subject: [PATCH 11/18] Attempt to clean up cross-platform isdigit reference. --- libraries/fbx/src/OBJReader.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 3421b3c366..d18f892c1b 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -15,6 +15,7 @@ #include #include +#include // This is the documented header for isdigit on MSVC. It should be unnecessary but safe on other platforms #include "FBXReader.h" #include "OBJReader.h" @@ -176,12 +177,6 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } -// Hmmm. Build report for Windows (which I don't have) reports that MSVC thinks isdigit isn't part of std lib. -// Fine. I'll roll my own. -bool fakeIsDigit(int character) { - return ('0' <= character) && (character <= '9'); -} - bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess) { FaceGroup faces; FBXMesh& mesh = geometry.meshes[0]; @@ -248,7 +243,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index QByteArray token = tokenizer.getDatum(); - if (!fakeIsDigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; + if (!std::isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } From 2687a7b038f3f655339ea17f4ed9dc7df0729d4f Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 30 Apr 2015 08:25:32 -0700 Subject: [PATCH 12/18] Take 2 on making MSVC happy. --- libraries/fbx/src/OBJReader.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index d18f892c1b..4473075e20 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -15,7 +15,7 @@ #include #include -#include // This is the documented header for isdigit on MSVC. It should be unnecessary but safe on other platforms +#include // .obj files are not locale-specific. The C/ASCII charset applies. #include "FBXReader.h" #include "OBJReader.h" @@ -243,7 +243,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi // vertex-index/texture-index // vertex-index/texture-index/surface-normal-index QByteArray token = tokenizer.getDatum(); - if (!std::isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; + if (!isdigit(token[0])) { // Tokenizer treats line endings as whitespace. Non-digit indicates done; tokenizer.pushBackToken(OBJTokenizer::DATUM_TOKEN); break; } From 2da8e1ab30ca509d02599078593fb8a475417e6c Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 30 Apr 2015 08:53:19 -0700 Subject: [PATCH 13/18] Pick up an upstream/master change that didn't get picked up in a merge of my fork/branch. --- libraries/render-utils/src/GeometryCache.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index b5c60a7834..32ea684b7c 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -1796,7 +1796,9 @@ NetworkGeometry::NetworkGeometry(const QUrl& url, const QSharedPointer(), -1 }; + FBXJoint joint = { false, QVector(), -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}; _geometry.joints.append(joint); _geometry.leftEyeJointIndex = -1; _geometry.rightEyeJointIndex = -1; From bf45f865a4ad4f30c4dd77869eb59afcef77e16d Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Thu, 30 Apr 2015 10:07:46 -0700 Subject: [PATCH 14/18] Codding standards, and fix an unintentionally duplicated declaration. --- libraries/fbx/src/OBJReader.cpp | 5 ++--- libraries/fbx/src/OBJReader.h | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 4473075e20..328dcdca5e 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -368,8 +368,7 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { geometry.meshExtents.addPoint(vertex); } //fbxDebugDump(geometry); - } - catch(const std::exception& e) { + } catch(const std::exception& e) { qCDebug(modelformat) << "something went wrong in OBJ reader: " << e.what(); } @@ -378,7 +377,7 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { -void OBJReader::fbxDebugDump(const FBXGeometry& fbxgeo) { +void fbxDebugDump(const FBXGeometry& fbxgeo) { qCDebug(modelformat) << "---------------- fbxGeometry ----------------"; qCDebug(modelformat) << " hasSkeletonJoints =" << fbxgeo.hasSkeletonJoints; qCDebug(modelformat) << " offset =" << fbxgeo.offset; diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 1734518493..0743cdeb89 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -53,10 +53,9 @@ public: QVector faceGroups; FBXGeometry readOBJ(const QByteArray& model, const QVariantHash& mapping); FBXGeometry readOBJ(QIODevice* device, const QVariantHash& mapping); - void fbxDebugDump(const FBXGeometry& fbxgeo); bool parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mapping, FBXGeometry& geometry, float& scaleGuess); }; -// What are these utilities doing here? Apparently used by fbx loading code in VHACD Utils. -void setMeshPartDefaults(FBXMeshPart &meshPart, QString materialID); +// 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 From e1d47413cc6b5e7078778ab3ecc331814e057395 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 1 May 2015 20:54:25 -0700 Subject: [PATCH 15/18] Material library handling. This is now functionally complete, but it needs testing, and the some tidying up. --- libraries/fbx/src/OBJReader.cpp | 183 +++++++++++++++++-- libraries/fbx/src/OBJReader.h | 33 +++- libraries/render-utils/src/GeometryCache.cpp | 21 +-- 3 files changed, 192 insertions(+), 45 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 328dcdca5e..45010fb09d 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -15,8 +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" @@ -26,6 +30,7 @@ QHash COMMENT_SCALE_HINTS = {{"This file uses centimeters as units", 1.0f / 100.0f}, {"This file uses millimeters as units", 1.0f / 1000.0f}}; +const QString SMART_DEFAULT_MATERIAL_NAME = "High Fidelity smart default material name"; OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } @@ -158,10 +163,12 @@ QVector OBJFace::triangulate() { newFaces.append(*this); } else { for (int i = 1; i < vertexIndices.count() - 1; i++) { - OBJFace newFace; // FIXME: also copy materialName, groupName + OBJFace newFace; newFace.addFrom(this, 0); newFace.addFrom(this, i); newFace.addFrom(this, i + 1); + newFace.groupName = groupName; + newFace.materialName = materialName; newFaces.append(newFace); } } @@ -177,6 +184,69 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } +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") { + if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { + return; + } + currentMaterial.diffuseTextureFilename = tokenizer.getDatum(); + } else if (token == "map_Ks") { + if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { + return; + } + currentMaterial.diffuseTextureFilename = tokenizer.getDatum(); + } + } +} + +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]; @@ -185,6 +255,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi bool sawG = false; bool result = true; int originalFaceCountForDebugging = 0; + QString currentGroup; setMeshPartDefaults(meshPart, QString("dontknow") + QString::number(mesh.parts.count())); @@ -220,8 +291,32 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi 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)); + 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") { vertices.append(tokenizer.getVec3()); } else if (token == "vn") { @@ -253,7 +348,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi // FIXME: if we want to handle negative indices, it has to be done here. const QByteArray noData {}; face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData); - // FIXME: preserve current name, material and such + face.groupName = currentGroup; + face.materialName = currentMaterialName; } originalFaceCountForDebugging++; foreach(OBJFace face, face.triangulate()) { @@ -278,18 +374,19 @@ done: 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 OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { +FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, QUrl* url) { FBXGeometry geometry; OBJTokenizer tokenizer(device); float scaleGuess = 1.0f; + 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. @@ -321,11 +418,56 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { 0, 0, 0, 1); mesh.clusters.append(cluster); - int meshPartCount = 0; - for (int i = 0; i < mesh.parts.count(); i++) { + // 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]; + QUrl candidateUrl = url->resolved(QUrl(candidateString)); + QNetworkReply *netReply = request(candidateUrl, true); + if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) { + textName = candidateString; + netReply->deleteLater(); + break; + } + netReply->deleteLater(); + } + 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]; - //qCDebug(modelformat) << "part:" << meshPartCount << " faces:" << faceGroups[meshPartCount].count() << "triangle indices will start with:" << mesh.vertices.count(); - foreach(OBJFace face, faceGroups[meshPartCount]) { + 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 ((leadFace.textureUVIndices.count() > 0) && groupMaterialName.isEmpty()) { + groupMaterialName = SMART_DEFAULT_MATERIAL_NAME; + } + if (!groupMaterialName.isEmpty()) { + if (!materials.contains(groupMaterialName)) { + qCDebug(modelformat) << "OBJ Reader:" << url << " references undefined material " << groupMaterialName << "."; + } else { + 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]]; @@ -352,7 +494,6 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { << textureUVs[face.textureUVIndices[2]]; } } - meshPartCount++; } // if we got a hint about units, scale all the points @@ -367,9 +508,9 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping) { mesh.meshExtents.addPoint(vertex); geometry.meshExtents.addPoint(vertex); } - //fbxDebugDump(geometry); + fbxDebugDump(geometry); } catch(const std::exception& e) { - qCDebug(modelformat) << "something went wrong in OBJ reader: " << e.what(); + qCDebug(modelformat) << "OBJ reader fail: " << e.what(); } return geometry; @@ -386,11 +527,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(); @@ -403,13 +544,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 0743cdeb89..16b13e00bf 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -1,5 +1,5 @@ - +#include #include "FBXReader.h" class OBJTokenizer { @@ -20,9 +20,9 @@ public: 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: - float getFloat() { return std::stof((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data()); } QIODevice* _device; QByteArray _datum; int _pushedBackToken; @@ -34,7 +34,8 @@ public: QVector vertexIndices; QVector textureUVIndices; QVector normalIndices; - //materialName groupName // FIXME + 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); // Return a set of one or more OBJFaces from this one, in which each is just a triangle. @@ -44,16 +45,38 @@ private: void addFrom(const OBJFace* face, int index); }; -class OBJReader { +// 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); + 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); }; // What are these utilities doing here? One is used by fbx loading code in VHACD Utils, and the other a general debugging utility. diff --git a/libraries/render-utils/src/GeometryCache.cpp b/libraries/render-utils/src/GeometryCache.cpp index 32ea684b7c..b341fe7dd5 100644 --- a/libraries/render-utils/src/GeometryCache.cpp +++ b/libraries/render-utils/src/GeometryCache.cpp @@ -2107,26 +2107,7 @@ void GeometryReader::run() { } fbxgeo = readFBX(_reply, _mapping, grabLightmaps, lightmapLevel); } else if (_url.path().toLower().endsWith(".obj")) { - fbxgeo = OBJReader().readOBJ(_reply, _mapping); - FBXMesh& mesh = fbxgeo.meshes[0]; // only one, by construction - if (mesh.texCoords.count() > 0) { // If we have uv texture coordinates.... - // ... then ensure that every meshPart has a texture filename. - // For now, that's defined directly, using the popular .obj convention that - // the texture is the same as the model basename. Later, we'll extend that - // with separate material files, too. - QString filename = _url.fileName(); - int extIndex = filename.lastIndexOf('.'); // by construction, this does not fail - QString basename = filename.remove(extIndex + 1, sizeof("obj")); - QByteArray defaultTexture = basename.toUtf8() + "jpg"; - //qCDebug(renderutils) << "basename for " << filename << " is " << basename << ", default:" << defaultTexture; - QVector& meshParts = mesh.parts; - for (int i = 0; i < meshParts.count(); i++) { - FBXMeshPart& meshPart = meshParts[i]; - if (meshPart.diffuseTexture.filename.count() == 0) { - meshPart.diffuseTexture.filename = defaultTexture; - } - } - } + fbxgeo = OBJReader().readOBJ(_reply, _mapping, &_url); } QMetaObject::invokeMethod(geometry.data(), "setGeometry", Q_ARG(const FBXGeometry&, fbxgeo)); } else { From eb05099261dfc87d14b32067f7302b09e4597db0 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Fri, 1 May 2015 21:39:19 -0700 Subject: [PATCH 16/18] (Try to) Address header file-location-discrepency between xcode and the jenkinds build environment. --- libraries/fbx/src/OBJReader.cpp | 4 ++-- libraries/fbx/src/OBJReader.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 45010fb09d..86f75f614c 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -15,8 +15,8 @@ #include #include -#include -#include +#include +#include #include #include // .obj files are not locale-specific. The C/ASCII charset applies. diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 16b13e00bf..65a65c70ee 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -1,5 +1,5 @@ -#include +#include #include "FBXReader.h" class OBJTokenizer { From d2dbb2c116dfd20d2a25eae23a7cab1e347607f8 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 6 May 2015 11:57:34 -0700 Subject: [PATCH 17/18] Missing the code! --- libraries/fbx/src/OBJReader.cpp | 81 +++++++++++++++++++-------------- libraries/fbx/src/OBJReader.h | 4 +- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index 86f75f614c..f78c35b8a0 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -35,6 +35,10 @@ const QString SMART_DEFAULT_MATERIAL_NAME = "High Fidelity smart default materia OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) { } +const QByteArray OBJTokenizer::getLineAsDatum() { + return _device->readLine().trimmed(); +} + int OBJTokenizer::nextToken() { if (_pushedBackToken != NO_PUSHBACKED_TOKEN) { int token = _pushedBackToken; @@ -133,7 +137,7 @@ void setMeshPartDefaults(FBXMeshPart& meshPart, QString materialID) { } // OBJFace -bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex) { +bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex, const QVector& vertices) { bool ok; int index = vertexIndex.toInt(&ok); if (!ok) { @@ -145,6 +149,9 @@ bool OBJFace::add(const QByteArray& vertexIndex, const QByteArray& textureIndex, 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()) { @@ -184,6 +191,14 @@ void OBJFace::addFrom(const OBJFace* face, int index) { // add using data from f } } +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; @@ -221,16 +236,21 @@ void OBJReader::parseMaterialLibrary(QIODevice* device) { currentMaterial.diffuseColor = tokenizer.getVec3(); } else if (token == "Ks") { currentMaterial.specularColor = tokenizer.getVec3(); - } else if (token == "map_Kd") { - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - return; + } 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; } - currentMaterial.diffuseTextureFilename = tokenizer.getDatum(); - } else if (token == "map_Ks") { - if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) { - return; + 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; } - currentMaterial.diffuseTextureFilename = tokenizer.getDatum(); } } } @@ -302,7 +322,7 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi break; // Some files use mtllib over and over again for the same libraryName } librariesSeen[libraryName] = true; - QUrl libraryUrl = url->resolved(QUrl(libraryName)); + 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)) { @@ -345,9 +365,8 @@ bool OBJReader::parseOBJGroup(OBJTokenizer& tokenizer, const QVariantHash& mappi QList parts = token.split('/'); assert(parts.count() >= 1); assert(parts.count() <= 3); - // FIXME: if we want to handle negative indices, it has to be done here. const QByteArray noData {}; - face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData); + face.add(parts[0], (parts.count() > 1) ? parts[1] : noData, (parts.count() > 2) ? parts[2] : noData, vertices); face.groupName = currentGroup; face.materialName = currentMaterialName; } @@ -428,14 +447,10 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, Q QByteArray base = basename.toUtf8(), textName = ""; for (int i = 0; i < extensions.count(); i++) { QByteArray candidateString = base + extensions[i]; - QUrl candidateUrl = url->resolved(QUrl(candidateString)); - QNetworkReply *netReply = request(candidateUrl, true); - if (netReply->isFinished() && (netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200)) { + if (isValidTexture(candidateString)) { textName = candidateString; - netReply->deleteLater(); break; } - netReply->deleteLater(); } if (!textName.isEmpty()) { preDefinedMaterial.diffuseTextureFilename = textName; @@ -447,24 +462,24 @@ FBXGeometry OBJReader::readOBJ(QIODevice* device, const QVariantHash& mapping, Q 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 ((leadFace.textureUVIndices.count() > 0) && groupMaterialName.isEmpty()) { + 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()) { - if (!materials.contains(groupMaterialName)) { - qCDebug(modelformat) << "OBJ Reader:" << url << " references undefined material " << groupMaterialName << "."; - } else { - 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); - } + 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) { diff --git a/libraries/fbx/src/OBJReader.h b/libraries/fbx/src/OBJReader.h index 65a65c70ee..a61665cb86 100644 --- a/libraries/fbx/src/OBJReader.h +++ b/libraries/fbx/src/OBJReader.h @@ -14,6 +14,7 @@ public: 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); } @@ -37,7 +38,7 @@ public: QString groupName; // We don't make use of hierarchical structure, but it can be preserved for debugging and future use. QString materialName; // Add one more set of vertex data. Answers true if successful - bool add(const QByteArray& vertexIndex, const QByteArray& textureIndex, const QByteArray& normalIndex); + bool add(const 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(); @@ -77,6 +78,7 @@ 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. From 35337ef2c209f56c0d3637ef2564125c775638d6 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 6 May 2015 12:28:45 -0700 Subject: [PATCH 18/18] Spell out order-of-evaluation for the compiler. --- libraries/fbx/src/OBJReader.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libraries/fbx/src/OBJReader.cpp b/libraries/fbx/src/OBJReader.cpp index f78c35b8a0..ebfa9b54f0 100644 --- a/libraries/fbx/src/OBJReader.cpp +++ b/libraries/fbx/src/OBJReader.cpp @@ -101,7 +101,10 @@ bool OBJTokenizer::isNextTokenFloat() { } glm::vec3 OBJTokenizer::getVec3() { - auto v = glm::vec3(getFloat(), getFloat(), getFloat()); // N.B.: getFloat() has side-effect + 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();