1076 lines · 46.8 KB · C++
RawDownload
////  OBJSerializer.cpp//  libraries/model-serializers/src////  Created by Seth Alves on 3/7/15.//  Copyright 2013 High Fidelity, Inc.//  Copyright 2022 Overte e.V.////  Distributed under the Apache License, Version 2.0.//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html//// http://en.wikipedia.org/wiki/Wavefront_.obj_file// http://www.scratchapixel.com/old/lessons/3d-advanced-lessons/obj-file-format/obj-file-format/// http://paulbourke.net/dataformats/obj/ #include "OBJSerializer.h" #include <ctype.h>  // .obj files are not locale-specific. The C/ASCII charset applies.#include <sstream> #include <QtCore/QBuffer>#include <QtCore/QIODevice>#include <QtCore/QEventLoop>#include <QtNetwork/QNetworkAccessManager>#include <QtNetwork/QNetworkRequest> #include <shared/NsightHelpers.h>#include <NetworkAccessManager.h>#include <ResourceManager.h> #include "FBXSerializer.h"#include <hfm/ModelFormatLogging.h>#include <shared/PlatformHacks.h> QHash<QString, float> 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"; const float ILLUMINATION_MODEL_MIN_OPACITY = 0.1f;const float ILLUMINATION_MODEL_APPLY_SHININESS = 0.5f;const float ILLUMINATION_MODEL_APPLY_ROUGHNESS = 1.0f;const float ILLUMINATION_MODEL_APPLY_NON_METALLIC = 0.0f; namespace {template<class T>T& checked_at(QVector<T>& vector, int i) {    if (i < 0 || i >= vector.size()) {        throw std::out_of_range("index " + std::to_string(i) + "is out of range");    }    return vector[i];}} OBJTokenizer::OBJTokenizer(QIODevice* device) : _device(device), _pushedBackToken(-1) {} const hifi::ByteArray OBJTokenizer::getLineAsDatum() {    return _device->readLine().trimmed();} float OBJTokenizer::getFloat() {    std::istringstream ss((nextToken() != OBJTokenizer::DATUM_TOKEN) ? nullptr : getDatum().data());    ss.imbue(std::locale::classic());    float f;    ss >> f;    return f;} int OBJTokenizer::nextToken(bool allowSpaceChar /*= false*/) {    if (_pushedBackToken != NO_PUSHBACKED_TOKEN) {        int token = _pushedBackToken;        _pushedBackToken = NO_PUSHBACKED_TOKEN;        return token;    }     char ch;    while (_device->getChar(&ch)) {        if (QChar(ch).isSpace()) {            continue; // skip whitespace        }        switch (ch) {            case '#': {                _datum = "";                _comment = _device->readLine(); // stash comment for a future call to getComment                return COMMENT_TOKEN;            }             case '\"':                _datum = "";                while (_device->getChar(&ch)) {                    if (ch == '\"') { // end on closing quote                        break;                    }                    if (ch == '\\') { // handle escaped quotes                        if (_device->getChar(&ch) && ch != '\"') {                            _datum.append('\\');                        }                    }                    _datum.append(ch);                }                return DATUM_TOKEN;             default:                _datum = "";                _datum.append(ch);                while (_device->getChar(&ch)) {                    if ((QChar(ch).isSpace() || ch == '\"') && (!allowSpaceChar || ch != ' ')) {                        ungetChar(ch); // read until we encounter a special character, then replace it                        break;                    }                    _datum.append(ch);                }                 return DATUM_TOKEN;        }    }    return NO_TOKEN;} bool OBJTokenizer::isNextTokenFloat() {    if (nextToken() != OBJTokenizer::DATUM_TOKEN) {        return false;    }    hifi::ByteArray token = getDatum();    pushBackToken(OBJTokenizer::DATUM_TOKEN);    bool ok;    token.toFloat(&ok);    return ok;} 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()) {        // ignore any following floats        nextToken();    }    return v;}bool OBJTokenizer::getVertex(glm::vec3& vertex, glm::vec3& vertexColor) {    // Used for vertices which may also have a vertex color (RGB [0,1]) to follow.    //    NOTE: Returns true if there is a vertex color.    auto x = getFloat(); // N.B.: getFloat() has side-effect    auto y = getFloat(); // And order of arguments is different on Windows/Linux.    auto z = getFloat();    vertex = glm::vec3(x, y, z);     auto r = 1.0f, g = 1.0f, b = 1.0f;    bool hasVertexColor = false;    if (isNextTokenFloat()) {        // If there's another float it's one of two things: a W component or an R component. The standard OBJ spec        // doesn't output a W component, so we're making the assumption that if a float follows (unless it's        // only a single value) that it's a vertex color.        r = getFloat();        if (isNextTokenFloat()) {            // Safe to assume the following values are the green/blue components.            g = getFloat();            b = getFloat();             hasVertexColor = true;        }         vertexColor = glm::vec3(r, g, b);    }     return hasVertexColor;}glm::vec2 OBJTokenizer::getVec2() {    float uCoord = getFloat();    float vCoord = 1.0f - getFloat();    auto v = glm::vec2(uCoord, vCoord);    while (isNextTokenFloat()) {        // there can be a w, but we don't handle that        nextToken();    }    return v;}  void setMeshPartDefaults(HFMMeshPart& meshPart, QString materialID) {    meshPart.materialID = materialID;} // OBJFace//    NOTE (trent, 7/20/17): The vertexColors vector being passed-in isn't necessary here, but I'm just//                         pairing it with the vertices vector for consistency.bool OBJFace::add(const hifi::ByteArray& vertexIndex, const hifi::ByteArray& textureIndex, const hifi::ByteArray& normalIndex, const QVector<glm::vec3>& vertices, const QVector<glm::vec3>& vertexColors) {    bool ok;    int index = vertexIndex.toInt(&ok);    if (!ok) {        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> OBJFace::triangulate() {    QVector<OBJFace> 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 OBJSerializer::isValidTexture(const hifi::ByteArray &filename) {    if (_url.isEmpty()) {        return false;    }    hifi::URL candidateUrl = _url.resolved(hifi::URL(filename));     return DependencyManager::get<ResourceManager>()->resourceExists(candidateUrl);} void OBJSerializer::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:                #ifdef WANT_DEBUG                qCDebug(modelformat) << "OBJSerializer MTLLIB comment:" << tokenizer.getComment();                #endif                break;            case OBJTokenizer::DATUM_TOKEN:                break;            default:                materials[matName] = currentMaterial;                #ifdef WANT_DEBUG                qCDebug(modelformat) <<                                     "OBJSerializer Last material illumination model:" << currentMaterial.illuminationModel <<                                     " shininess:" << currentMaterial.shininess <<                                     " opacity:" << currentMaterial.opacity <<                                     " diffuse color:" << currentMaterial.diffuseColor <<                                     " specular color:" << currentMaterial.specularColor <<                                     " emissive color:" << currentMaterial.emissiveColor <<                                     " diffuse texture:" << currentMaterial.diffuseTextureFilename <<                                     " specular texture:" << currentMaterial.specularTextureFilename <<                                     " emissive texture:" << currentMaterial.emissiveTextureFilename <<                                     " bump texture:" << currentMaterial.bumpTextureFilename <<                                     " opacity texture:" << currentMaterial.opacityTextureFilename;#endif                return;        }        hifi::ByteArray token = tokenizer.getDatum();        if (token == "newmtl") {            if (tokenizer.nextToken() != OBJTokenizer::DATUM_TOKEN) {                return;            }            materials[matName] = currentMaterial;            matName = tokenizer.getDatum();            currentMaterial = materials[matName];            #ifdef WANT_DEBUG            qCDebug(modelformat) << "OBJSerializer Starting new material definition " << matName;            #endif            currentMaterial.diffuseTextureFilename = "";            currentMaterial.emissiveTextureFilename = "";            currentMaterial.specularTextureFilename = "";            currentMaterial.bumpTextureFilename = "";            currentMaterial.opacityTextureFilename = "";         } else if (token == "Ns") {            currentMaterial.shininess = tokenizer.getFloat();        } else if (token == "Ni") {            #ifdef WANT_DEBUG            qCDebug(modelformat) << "OBJSerializer Ignoring material Ni " << tokenizer.getFloat();            #else            tokenizer.getFloat();            #endif        } else if (token == "d") {            currentMaterial.opacity = tokenizer.getFloat();        } else if (token == "Tr") {            currentMaterial.opacity = 1.0f - tokenizer.getFloat();        } else if (token == "illum") {            currentMaterial.illuminationModel = tokenizer.getFloat();        } else if (token == "Tf") {            #ifdef WANT_DEBUG            qCDebug(modelformat) << "OBJSerializer Ignoring material Tf " << tokenizer.getVec3();            #else            tokenizer.getVec3();            #endif        } else if (token == "Ka") {            #ifdef WANT_DEBUG            qCDebug(modelformat) << "OBJSerializer Ignoring material Ka " << tokenizer.getVec3();;            #else            tokenizer.getVec3();            #endif        } else if (token == "Kd") {            currentMaterial.diffuseColor = tokenizer.getVec3();        } else if (token == "Ke") {            currentMaterial.emissiveColor = tokenizer.getVec3();        } else if (token == "Ks") {            currentMaterial.specularColor = tokenizer.getVec3();        } else if ((token == "map_Kd") || (token == "map_Ke") || (token == "map_Ks") || (token == "map_bump") || (token == "bump") || (token == "map_d")) {            const hifi::ByteArray textureLine = tokenizer.getLineAsDatum();            hifi::ByteArray filename;            OBJMaterialTextureOptions textureOptions;            parseTextureLine(textureLine, filename, textureOptions);            if (filename.endsWith(".tga")) {                #ifdef WANT_DEBUG                qCDebug(modelformat) << "OBJSerializer WARNING: currently ignoring tga texture " << filename << " in " << _url;                #endif                break;            }            if (token == "map_Kd") {                currentMaterial.diffuseTextureFilename = filename;            } else if (token == "map_Ke") {                currentMaterial.emissiveTextureFilename = filename;            } else if (token == "map_Ks" ) {                currentMaterial.specularTextureFilename = filename;            } else if ((token == "map_bump") || (token == "bump")) {                currentMaterial.bumpTextureFilename = filename;                currentMaterial.bumpTextureOptions = textureOptions;            } else if (token == "map_d") {                currentMaterial.opacityTextureFilename = filename;            }        }    }} void OBJSerializer::parseTextureLine(const hifi::ByteArray& textureLine, hifi::ByteArray& filename, OBJMaterialTextureOptions& textureOptions) {    // Texture options reference http://paulbourke.net/dataformats/mtl/    // and https://wikivisually.com/wiki/Material_Template_Library     std::istringstream iss(textureLine.toStdString());    const std::vector<std::string> parser(std::istream_iterator<std::string>{iss}, std::istream_iterator<std::string>());     uint i = 0;    while (i < parser.size()) {        if (i + 1 < parser.size() && parser[i][0] == '-') {            const std::string& option = parser[i++];            if (option == "-blendu" || option == "-blendv") {                #ifdef WANT_DEBUG                const std::string& onoff = parser[i++];                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << onoff.c_str();                #endif            } else if (option == "-bm") {                const std::string& bm = parser[i++];                textureOptions.bumpMultiplier = std::stof(bm);            } else if (option == "-boost") {                #ifdef WANT_DEBUG                const std::string& boost = parser[i++];                float boostFloat = std::stof(boost);                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << boost.c_str();                #endif            } else if (option == "-cc") {                #ifdef WANT_DEBUG                const std::string& onoff = parser[i++];                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << onoff.c_str();                #endif            } else if (option == "-clamp") {                #ifdef WANT_DEBUG                const std::string& onoff = parser[i++];                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << onoff.c_str();                #endif            } else if (option == "-imfchan") {                #ifdef WANT_DEBUG                const std::string& imfchan = parser[i++];                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << imfchan.c_str();                #endif            } else if (option == "-mm") {                if (i + 1 < parser.size()) {                    #ifdef WANT_DEBUG                    const std::string& mmBase = parser[i++];                    const std::string& mmGain = parser[i++];                    float mmBaseFloat = std::stof(mmBase);                    float mmGainFloat = std::stof(mmGain);                    qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << mmBase.c_str() << mmGain.c_str();                    #endif                }            } else if (option == "-o" || option == "-s" || option == "-t") {                if (i + 2 < parser.size()) {                    #ifdef WANT_DEBUG                    const std::string& u = parser[i++];                    const std::string& v = parser[i++];                    const std::string& w = parser[i++];                    float uFloat = std::stof(u);                    float vFloat = std::stof(v);                    float wFloat = std::stof(w);                    qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << u.c_str() << v.c_str() << w.c_str();                    #endif                }            } else if (option == "-texres") {                #ifdef WANT_DEBUG                const std::string& texres = parser[i++];                float texresFloat = std::stof(texres);                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << texres.c_str();                #endif            } else if (option == "-type") {                #ifdef WANT_DEBUG                const std::string& type = parser[i++];                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring texture option" << option.c_str() << type.c_str();                #endif            } else if (option[0] == '-') {                #ifdef WANT_DEBUG                qCDebug(modelformat) << "OBJSerializer WARNING: Ignoring unsupported texture option" << option.c_str();                #endif            }        } else { // assume filename at end when no more options            std::string filenameString = parser[i++];            while (i < parser.size()) { // filename has space in it                filenameString += " " + parser[i++];            }            filename = filenameString.c_str();        }    }} std::tuple<bool, hifi::ByteArray> requestData(hifi::URL& url) {    auto request = DependencyManager::get<ResourceManager>()->createResourceRequest(        nullptr, url, true, -1, "(OBJSerializer) requestData");     if (!request) {        return std::make_tuple(false, hifi::ByteArray());    }     QEventLoop loop;    QObject::connect(request, &ResourceRequest::finished, &loop, &QEventLoop::quit);    request->send();    loop.exec();     if (request->getResult() == ResourceRequest::Success) {        return std::make_tuple(true, request->getData());    } else {        return std::make_tuple(false, hifi::ByteArray());    }}  QNetworkReply* request(hifi::URL& url, bool isTest) {    if (!qApp) {        return nullptr;    }    bool aboutToQuit{ false };    auto connection = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [&] {        aboutToQuit = true;    });    QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();    QNetworkRequest netRequest(url);    netRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);    QNetworkReply* netReply = isTest ? networkAccessManager.head(netRequest) : networkAccessManager.get(netRequest);    if (!qApp || aboutToQuit) {        netReply->deleteLater();        return nullptr;    }    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     QObject::disconnect(connection);    return netReply;                // trying to sync later on.}  bool OBJSerializer::parseOBJGroup(OBJTokenizer& tokenizer, const hifi::VariantHash& mapping, HFMModel& hfmModel,                              float& scaleGuess, bool combineParts) {    FaceGroup faces;    HFMMesh& mesh = hfmModel.meshes[0];    mesh.parts.append(HFMMeshPart());

Lines 1–500 of 1076.