diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index b0b769d5e9..bb7efb8f75 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -1,53 +1,101 @@ // // ModelCache.cpp -// interface/src/renderer +// libraries/model-networking // -// Created by Andrzej Kapolka on 6/21/13. -// Copyright 2013 High Fidelity, Inc. +// Created by Zach Pomerantz on 3/15/16. +// Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "ModelCache.h" +#include +#include "FBXReader.h" +#include "OBJReader.h" -#include +#include +#include -#include #include -#include -#include - -#include "TextureCache.h" #include "ModelNetworkingLogging.h" -#include "model/TextureMap.h" +class GeometryReader; -//#define WANT_DEBUG +class GeometryExtra { +public: + const QVariantHash& mapping; + const QUrl& textureBaseUrl; +}; -ModelCache::ModelCache() -{ - const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; - setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); +class GeometryMappingResource : public GeometryResource { + Q_OBJECT +public: + GeometryMappingResource(const QUrl& url) : GeometryResource(url) {}; + + virtual void downloadFinished(const QByteArray& data) override; + +private slots: + void onGeometryMappingLoaded(bool success); + +private: + GeometryResource::Pointer _geometryResource; +}; + +void GeometryMappingResource::downloadFinished(const QByteArray& data) { + auto mapping = FSTReader::readMapping(data); + + QString filename = mapping.value("filename").toString(); + if (filename.isNull()) { + qCDebug(modelnetworking) << "Mapping file" << _url << "has no \"filename\" field"; + finishedLoading(false); + } else { + QUrl url = _url.resolved(filename); + QUrl textureBaseUrl; + + QString texdir = mapping.value("texdir").toString(); + if (!texdir.isNull()) { + if (!texdir.endsWith('/')) { + texdir += '/'; + } + textureBaseUrl = _url.resolved(texdir); + } + + auto modelCache = DependencyManager::get(); + GeometryExtra extra{ mapping, textureBaseUrl }; + + // Get the raw GeometryResource, not the wrapped NetworkGeometry + _geometryResource = modelCache->getResource(url, QUrl(), true, &extra).staticCast(); + connect(_geometryResource.data(), &Resource::finished, this, &GeometryMappingResource::onGeometryMappingLoaded); + } } -ModelCache::~ModelCache() { +void GeometryMappingResource::onGeometryMappingLoaded(bool success) { + if (success) { + _geometry = _geometryResource->_geometry; + _shapes = _geometryResource->_shapes; + _meshes = _geometryResource->_meshes; + _materials = _geometryResource->_materials; + } + finishedLoading(success); } -QSharedPointer ModelCache::createResource(const QUrl& url, const QSharedPointer& fallback, - bool delayLoad, const void* extra) { - // NetworkGeometry is no longer a subclass of Resource, but requires this method because, it is pure virtual. - assert(false); - return QSharedPointer(); -} +class GeometryReader : public QRunnable { +public: + GeometryReader(QWeakPointer& resource, const QUrl& url, const QVariantHash& mapping, + const QByteArray& data) : + _resource(resource), _url(url), _mapping(mapping), _data(data) {} + virtual ~GeometryReader() = default; + virtual void run() override; -GeometryReader::GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping) : - _url(url), - _data(data), - _mapping(mapping) { -} +private: + QWeakPointer _resource; + QUrl _url; + QVariantHash _mapping; + QByteArray _data; +}; void GeometryReader::run() { auto originalPriority = QThread::currentThread()->priority(); @@ -55,458 +103,353 @@ void GeometryReader::run() { originalPriority = QThread::NormalPriority; } QThread::currentThread()->setPriority(QThread::LowPriority); + + // Ensure the resource is still being requested + auto resource = _resource.toStrongRef(); + if (!resource) { + qCWarning(modelnetworking) << "Abandoning load of" << _url << "; could not get strong ref"; + return; + } + try { if (_data.isEmpty()) { - throw QString("Reply is NULL ?!"); + throw QString("reply is NULL"); } - QString urlname = _url.path().toLower(); - bool urlValid = true; - urlValid &= !urlname.isEmpty(); - urlValid &= !_url.path().isEmpty(); - urlValid &= _url.path().toLower().endsWith(".fbx") || _url.path().toLower().endsWith(".obj"); - if (urlValid) { - // Let's read the binaries from the network - FBXGeometry* fbxgeo = nullptr; + QString urlname = _url.path().toLower(); + if (!urlname.isEmpty() && !_url.path().isEmpty() && + (_url.path().toLower().endsWith(".fbx") || _url.path().toLower().endsWith(".obj"))) { + FBXGeometry* fbxGeometry = nullptr; + if (_url.path().toLower().endsWith(".fbx")) { - const bool grabLightmaps = true; - const float lightmapLevel = 1.0f; - fbxgeo = readFBX(_data, _mapping, _url.path(), grabLightmaps, lightmapLevel); - if (fbxgeo->meshes.size() == 0 && fbxgeo->joints.size() == 0) { - // empty fbx geometry, indicates error + fbxGeometry = readFBX(_data, _mapping, _url.path()); + if (fbxGeometry->meshes.size() == 0 && fbxGeometry->joints.size() == 0) { throw QString("empty geometry, possibly due to an unsupported FBX version"); } } else if (_url.path().toLower().endsWith(".obj")) { - fbxgeo = OBJReader().readOBJ(_data, _mapping, _url); + fbxGeometry = OBJReader().readOBJ(_data, _mapping, _url); } else { - QString errorStr("unsupported format"); - throw errorStr; + throw QString("unsupported format"); } - emit onSuccess(fbxgeo); + + QMetaObject::invokeMethod(resource.data(), "setGeometryDefinition", + Q_ARG(void*, fbxGeometry)); } else { throw QString("url is invalid"); } - } catch (const QString& error) { qCDebug(modelnetworking) << "Error reading " << _url << ": " << error; - emit onError(NetworkGeometry::ModelParseError, error); + QMetaObject::invokeMethod(resource.data(), "finishedLoading", Q_ARG(bool, false)); } QThread::currentThread()->setPriority(originalPriority); } -NetworkGeometry::NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl) : - _url(url), - _mapping(mapping), - _textureBaseUrl(textureBaseUrl.isValid() ? textureBaseUrl : url) { +class GeometryDefinitionResource : public GeometryResource { + Q_OBJECT +public: + GeometryDefinitionResource(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) : + GeometryResource(url), _mapping(mapping), _textureBaseUrl(textureBaseUrl) {} - if (delayLoad) { - _state = DelayState; - } else { - attemptRequestInternal(); - } + virtual void downloadFinished(const QByteArray& data) override; + +protected: + Q_INVOKABLE void setGeometryDefinition(void* fbxGeometry); + +private: + QVariantHash _mapping; + QUrl _textureBaseUrl; +}; + +void GeometryDefinitionResource::downloadFinished(const QByteArray& data) { + QThreadPool::globalInstance()->start(new GeometryReader(_self, _url, _mapping, data)); } -NetworkGeometry::~NetworkGeometry() { - if (_resource) { - _resource->deleteLater(); - } -} +void GeometryDefinitionResource::setGeometryDefinition(void* fbxGeometry) { + // Assume ownership of the geometry pointer + _geometry.reset(static_cast(fbxGeometry)); -void NetworkGeometry::attemptRequest() { - if (_state == DelayState) { - attemptRequestInternal(); - } -} - -void NetworkGeometry::attemptRequestInternal() { - if (_url.path().toLower().endsWith(".fst")) { - _mappingUrl = _url; - requestMapping(_url); - } else { - _modelUrl = _url; - requestModel(_url); - } -} - -bool NetworkGeometry::isLoaded() const { - return _state == SuccessState; -} - -bool NetworkGeometry::isLoadedWithTextures() const { - if (!isLoaded()) { - return false; + // Copy materials + QHash materialIDAtlas; + for (const FBXMaterial& material : _geometry->materials) { + materialIDAtlas[material.materialID] = _materials.size(); + _materials.push_back(std::make_shared(material, _textureBaseUrl)); } - if (!_isLoadedWithTextures) { - _hasTransparentTextures = false; - - for (auto&& material : _materials) { - if ((material->albedoTexture && !material->albedoTexture->isLoaded()) || - (material->normalTexture && !material->normalTexture->isLoaded()) || - (material->roughnessTexture && !material->roughnessTexture->isLoaded()) || - (material->metallicTexture && !material->metallicTexture->isLoaded()) || - (material->occlusionTexture && !material->occlusionTexture->isLoaded()) || - (material->emissiveTexture && !material->emissiveTexture->isLoaded()) || - (material->lightmapTexture && !material->lightmapTexture->isLoaded())) { - return false; - } - if (material->albedoTexture && material->albedoTexture->getGPUTexture()) { - // Reassign the texture to make sure that itsalbedo alpha channel material key is detected correctly - material->_material->setTextureMap(model::MaterialKey::ALBEDO_MAP, material->_material->getTextureMap(model::MaterialKey::ALBEDO_MAP)); - const auto& usage = material->albedoTexture->getGPUTexture()->getUsage(); - bool isTransparentTexture = usage.isAlpha() && !usage.isAlphaMask(); - _hasTransparentTextures |= isTransparentTexture; - } - } - - _isLoadedWithTextures = true; - } - return true; -} - -void NetworkGeometry::setTextureWithNameToURL(const QString& name, const QUrl& url) { - if (_meshes.size() > 0) { - auto textureCache = DependencyManager::get(); - for (auto&& material : _materials) { - auto networkMaterial = material->_material; - auto oldTextureMaps = networkMaterial->getTextureMaps(); - if (material->albedoTextureName == name) { - material->albedoTexture = textureCache->getTexture(url, DEFAULT_TEXTURE); - - auto albedoMap = model::TextureMapPointer(new model::TextureMap()); - albedoMap->setTextureSource(material->albedoTexture->_textureSource); - albedoMap->setTextureTransform(oldTextureMaps[model::MaterialKey::ALBEDO_MAP]->getTextureTransform()); - // when reassigning the albedo texture we also check for the alpha channel used as opacity - albedoMap->setUseAlphaChannel(true); - networkMaterial->setTextureMap(model::MaterialKey::ALBEDO_MAP, albedoMap); - } else if (material->normalTextureName == name) { - material->normalTexture = textureCache->getTexture(url); - - auto normalMap = model::TextureMapPointer(new model::TextureMap()); - normalMap->setTextureSource(material->normalTexture->_textureSource); - - networkMaterial->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap); - } else if (material->roughnessTextureName == name) { - // FIXME: If passing a gloss map instead of a roughmap how to say that ? looking for gloss in the name ? - material->roughnessTexture = textureCache->getTexture(url, ROUGHNESS_TEXTURE); - - auto roughnessMap = model::TextureMapPointer(new model::TextureMap()); - roughnessMap->setTextureSource(material->roughnessTexture->_textureSource); - - networkMaterial->setTextureMap(model::MaterialKey::ROUGHNESS_MAP, roughnessMap); - } else if (material->metallicTextureName == name) { - // FIXME: If passing a specular map instead of a metallic how to say that ? looking for wtf in the name ? - material->metallicTexture = textureCache->getTexture(url, METALLIC_TEXTURE); - - auto glossMap = model::TextureMapPointer(new model::TextureMap()); - glossMap->setTextureSource(material->metallicTexture->_textureSource); - - networkMaterial->setTextureMap(model::MaterialKey::METALLIC_MAP, glossMap); - } else if (material->emissiveTextureName == name) { - material->emissiveTexture = textureCache->getTexture(url, EMISSIVE_TEXTURE); - - auto emissiveMap = model::TextureMapPointer(new model::TextureMap()); - emissiveMap->setTextureSource(material->emissiveTexture->_textureSource); - - networkMaterial->setTextureMap(model::MaterialKey::EMISSIVE_MAP, emissiveMap); - } else if (material->lightmapTextureName == name) { - material->lightmapTexture = textureCache->getTexture(url, LIGHTMAP_TEXTURE); - - auto lightmapMap = model::TextureMapPointer(new model::TextureMap()); - lightmapMap->setTextureSource(material->lightmapTexture->_textureSource); - lightmapMap->setTextureTransform( - oldTextureMaps[model::MaterialKey::LIGHTMAP_MAP]->getTextureTransform()); - glm::vec2 oldOffsetScale = - oldTextureMaps[model::MaterialKey::LIGHTMAP_MAP]->getLightmapOffsetScale(); - lightmapMap->setLightmapOffsetScale(oldOffsetScale.x, oldOffsetScale.y); - - networkMaterial->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap); - } - } - } else { - qCWarning(modelnetworking) << "Ignoring setTextureWithNameToURL() geometry not ready." << name << url; - } - _isLoadedWithTextures = false; -} - -QStringList NetworkGeometry::getTextureNames() const { - QStringList result; - for (auto&& material : _materials) { - if (!material->emissiveTextureName.isEmpty() && material->emissiveTexture) { - QString textureURL = material->emissiveTexture->getURL().toString(); - result << material->emissiveTextureName + ":\"" + textureURL + "\""; - } - - if (!material->albedoTextureName.isEmpty() && material->albedoTexture) { - QString textureURL = material->albedoTexture->getURL().toString(); - result << material->albedoTextureName + ":\"" + textureURL + "\""; - } - - if (!material->normalTextureName.isEmpty() && material->normalTexture) { - QString textureURL = material->normalTexture->getURL().toString(); - result << material->normalTextureName + ":\"" + textureURL + "\""; - } - - if (!material->roughnessTextureName.isEmpty() && material->roughnessTexture) { - QString textureURL = material->roughnessTexture->getURL().toString(); - result << material->roughnessTextureName + ":\"" + textureURL + "\""; - } - - if (!material->metallicTextureName.isEmpty() && material->metallicTexture) { - QString textureURL = material->metallicTexture->getURL().toString(); - result << material->metallicTextureName + ":\"" + textureURL + "\""; - } - - if (!material->occlusionTextureName.isEmpty() && material->occlusionTexture) { - QString textureURL = material->occlusionTexture->getURL().toString(); - result << material->occlusionTextureName + ":\"" + textureURL + "\""; - } - - if (!material->lightmapTextureName.isEmpty() && material->lightmapTexture) { - QString textureURL = material->lightmapTexture->getURL().toString(); - result << material->lightmapTextureName + ":\"" + textureURL + "\""; - } - } - - return result; -} - -void NetworkGeometry::requestMapping(const QUrl& url) { - _state = RequestMappingState; - if (_resource) { - _resource->deleteLater(); - } - _resource = new Resource(url, false); - connect(_resource, &Resource::loaded, this, &NetworkGeometry::mappingRequestDone); - connect(_resource, &Resource::failed, this, &NetworkGeometry::mappingRequestError); -} - -void NetworkGeometry::requestModel(const QUrl& url) { - _state = RequestModelState; - if (_resource) { - _resource->deleteLater(); - } - _modelUrl = url; - _resource = new Resource(url, false); - connect(_resource, &Resource::loaded, this, &NetworkGeometry::modelRequestDone); - connect(_resource, &Resource::failed, this, &NetworkGeometry::modelRequestError); -} - -void NetworkGeometry::mappingRequestDone(const QByteArray& data) { - assert(_state == RequestMappingState); - - // parse the mapping file - _mapping = FSTReader::readMapping(data); - - QUrl replyUrl = _mappingUrl; - QString modelUrlStr = _mapping.value("filename").toString(); - if (modelUrlStr.isNull()) { - qCDebug(modelnetworking) << "Mapping file " << _url << "has no \"filename\" entry"; - emit onFailure(*this, MissingFilenameInMapping); - } else { - // read _textureBase from mapping file, if present - QString texdir = _mapping.value("texdir").toString(); - if (!texdir.isNull()) { - if (!texdir.endsWith('/')) { - texdir += '/'; - } - _textureBaseUrl = replyUrl.resolved(texdir); - } - - _modelUrl = replyUrl.resolved(modelUrlStr); - requestModel(_modelUrl); - } -} - -void NetworkGeometry::mappingRequestError(QNetworkReply::NetworkError error) { - assert(_state == RequestMappingState); - _state = ErrorState; - emit onFailure(*this, MappingRequestError); -} - -void NetworkGeometry::modelRequestDone(const QByteArray& data) { - assert(_state == RequestModelState); - - _state = ParsingModelState; - - // asynchronously parse the model file. - GeometryReader* geometryReader = new GeometryReader(_modelUrl, data, _mapping); - connect(geometryReader, SIGNAL(onSuccess(FBXGeometry*)), SLOT(modelParseSuccess(FBXGeometry*))); - connect(geometryReader, SIGNAL(onError(int, QString)), SLOT(modelParseError(int, QString))); - - QThreadPool::globalInstance()->start(geometryReader); -} - -void NetworkGeometry::modelRequestError(QNetworkReply::NetworkError error) { - assert(_state == RequestModelState); - _state = ErrorState; - emit onFailure(*this, ModelRequestError); -} - -static NetworkMesh* buildNetworkMesh(const FBXMesh& mesh, const QUrl& textureBaseUrl) { - NetworkMesh* networkMesh = new NetworkMesh(); - - networkMesh->_mesh = mesh._mesh; - - return networkMesh; -} - - -static model::TextureMapPointer setupNetworkTextureMap(NetworkGeometry* geometry, const QUrl& textureBaseUrl, - const FBXTexture& texture, TextureType type, - NetworkTexturePointer& networkTexture, QString& networkTextureName) { - auto textureCache = DependencyManager::get(); - - // If content is inline, cache it under the fbx file, not its base url - const auto baseUrl = texture.content.isEmpty() ? textureBaseUrl : QUrl(textureBaseUrl.url() + "/"); - const auto filename = baseUrl.resolved(QUrl(texture.filename)); - - networkTexture = textureCache->getTexture(filename, type, texture.content); - QObject::connect(networkTexture.data(), &NetworkTexture::networkTextureCreated, geometry, &NetworkGeometry::textureLoaded); - networkTextureName = texture.name; - - auto map = std::make_shared(); - map->setTextureSource(networkTexture->_textureSource); - return map; -} - -static NetworkMaterial* buildNetworkMaterial(NetworkGeometry* geometry, const FBXMaterial& material, const QUrl& textureBaseUrl) { - NetworkMaterial* networkMaterial = new NetworkMaterial(); - networkMaterial->_material = material._material; - - if (!material.albedoTexture.filename.isEmpty()) { - auto albedoMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.albedoTexture, DEFAULT_TEXTURE, - networkMaterial->albedoTexture, networkMaterial->albedoTextureName); - albedoMap->setTextureTransform(material.albedoTexture.transform); - - if (!material.opacityTexture.filename.isEmpty()) { - if (material.albedoTexture.filename == material.opacityTexture.filename) { - // Best case scenario, just indicating that the albedo map contains transparency - albedoMap->setUseAlphaChannel(true); - } else { - // Opacity Map is different from the Abledo map, not supported - } - } - - material._material->setTextureMap(model::MaterialKey::ALBEDO_MAP, albedoMap); - } - - - if (!material.normalTexture.filename.isEmpty()) { - auto normalMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.normalTexture, - (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE), - networkMaterial->normalTexture, networkMaterial->normalTextureName); - networkMaterial->_material->setTextureMap(model::MaterialKey::NORMAL_MAP, normalMap); - } - - // Roughness first or gloss maybe - if (!material.roughnessTexture.filename.isEmpty()) { - auto roughnessMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.roughnessTexture, ROUGHNESS_TEXTURE, - networkMaterial->roughnessTexture, networkMaterial->roughnessTextureName); - material._material->setTextureMap(model::MaterialKey::ROUGHNESS_MAP, roughnessMap); - } else if (!material.glossTexture.filename.isEmpty()) { - auto roughnessMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.glossTexture, GLOSS_TEXTURE, - networkMaterial->roughnessTexture, networkMaterial->roughnessTextureName); - material._material->setTextureMap(model::MaterialKey::ROUGHNESS_MAP, roughnessMap); - } - - // Metallic first or specular maybe - - if (!material.metallicTexture.filename.isEmpty()) { - auto metallicMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.metallicTexture, METALLIC_TEXTURE, - networkMaterial->metallicTexture, networkMaterial->metallicTextureName); - material._material->setTextureMap(model::MaterialKey::METALLIC_MAP, metallicMap); - } else if (!material.specularTexture.filename.isEmpty()) { - - auto metallicMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.specularTexture, SPECULAR_TEXTURE, - networkMaterial->metallicTexture, networkMaterial->metallicTextureName); - material._material->setTextureMap(model::MaterialKey::METALLIC_MAP, metallicMap); - } - - if (!material.occlusionTexture.filename.isEmpty()) { - auto occlusionMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.occlusionTexture, OCCLUSION_TEXTURE, - networkMaterial->occlusionTexture, networkMaterial->occlusionTextureName); - material._material->setTextureMap(model::MaterialKey::OCCLUSION_MAP, occlusionMap); - } - - if (!material.emissiveTexture.filename.isEmpty()) { - auto emissiveMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.emissiveTexture, EMISSIVE_TEXTURE, - networkMaterial->emissiveTexture, networkMaterial->emissiveTextureName); - material._material->setTextureMap(model::MaterialKey::EMISSIVE_MAP, emissiveMap); - } - - if (!material.lightmapTexture.filename.isEmpty()) { - auto lightmapMap = setupNetworkTextureMap(geometry, textureBaseUrl, material.lightmapTexture, LIGHTMAP_TEXTURE, - networkMaterial->lightmapTexture, networkMaterial->lightmapTextureName); - lightmapMap->setTextureTransform(material.lightmapTexture.transform); - lightmapMap->setLightmapOffsetScale(material.lightmapParams.x, material.lightmapParams.y); - material._material->setTextureMap(model::MaterialKey::LIGHTMAP_MAP, lightmapMap); - } - - return networkMaterial; -} - - -void NetworkGeometry::modelParseSuccess(FBXGeometry* geometry) { - // assume owner ship of geometry pointer - _geometry.reset(geometry); - - - - foreach(const FBXMesh& mesh, _geometry->meshes) { - _meshes.emplace_back(buildNetworkMesh(mesh, _textureBaseUrl)); - } - - QHash fbxMatIDToMatID; - foreach(const FBXMaterial& material, _geometry->materials) { - fbxMatIDToMatID[material.materialID] = _materials.size(); - _materials.emplace_back(buildNetworkMaterial(this, material, _textureBaseUrl)); - } - - + std::shared_ptr meshes = std::make_shared(); + std::shared_ptr shapes = std::make_shared(); int meshID = 0; - foreach(const FBXMesh& mesh, _geometry->meshes) { + for (const FBXMesh& mesh : _geometry->meshes) { + // Copy mesh pointers + meshes->emplace_back(mesh._mesh); int partID = 0; - foreach (const FBXMeshPart& part, mesh.parts) { - NetworkShape* networkShape = new NetworkShape(); - networkShape->_meshID = meshID; - networkShape->_partID = partID; - networkShape->_materialID = (int)fbxMatIDToMatID[part.materialID]; - _shapes.emplace_back(networkShape); + for (const FBXMeshPart& part : mesh.parts) { + // Construct local shapes + shapes->emplace_back(meshID, partID, (int)materialIDAtlas[part.materialID]); partID++; } meshID++; } + _meshes = meshes; + _shapes = shapes; - _state = SuccessState; - emit onSuccess(*this, *_geometry.get()); - - delete _resource; - _resource = nullptr; + finishedLoading(true); } -void NetworkGeometry::modelParseError(int error, QString str) { - _state = ErrorState; - emit onFailure(*this, (NetworkGeometry::Error)error); - - delete _resource; - _resource = nullptr; +ModelCache::ModelCache() { + const qint64 GEOMETRY_DEFAULT_UNUSED_MAX_SIZE = DEFAULT_UNUSED_MAX_SIZE; + setUnusedResourceCacheSize(GEOMETRY_DEFAULT_UNUSED_MAX_SIZE); } -const NetworkMaterial* NetworkGeometry::getShapeMaterial(int shapeID) { - if ((shapeID >= 0) && (shapeID < (int)_shapes.size())) { - int materialID = _shapes[shapeID]->_materialID; - if ((materialID >= 0) && ((unsigned int)materialID < _materials.size())) { - return _materials[materialID].get(); - } else { - return 0; +QSharedPointer ModelCache::createResource(const QUrl& url, const QSharedPointer& fallback, + bool delayLoad, const void* extra) { + const GeometryExtra* geometryExtra = static_cast(extra); + + Resource* resource = nullptr; + if (url.path().toLower().endsWith(".fst")) { + resource = new GeometryMappingResource(url); + } else { + resource = new GeometryDefinitionResource(url, geometryExtra->mapping, geometryExtra->textureBaseUrl); + } + + return QSharedPointer(resource, &Resource::allReferencesCleared); +} + +std::shared_ptr ModelCache::getGeometry(const QUrl& url, const QVariantHash& mapping, const QUrl& textureBaseUrl) { + GeometryExtra geometryExtra = { mapping, textureBaseUrl }; + GeometryResource::Pointer resource = getResource(url, QUrl(), true, &geometryExtra).staticCast(); + return std::make_shared(resource); +} + +const QVariantMap Geometry::getTextures() const { + QVariantMap textures; + for (const auto& material : _materials) { + for (const auto& texture : material->_textures) { + if (texture.texture) { + textures[texture.name] = texture.texture->getURL(); + } + } + } + + return textures; +} + +void Geometry::setTextures(const QVariantMap& textureMap) { + if (_meshes->size() > 0) { + for (auto& material : _materials) { + // Check if any material textures actually changed + if (std::any_of(material->_textures.cbegin(), material->_textures.cend(), + [&textureMap](const NetworkMaterial::Textures::value_type& it) { return it.texture && textureMap.contains(it.name); })) { + + material = std::make_shared(*material, textureMap); + _areTexturesLoaded = false; + } } } else { - return 0; + qCWarning(modelnetworking) << "Ignoring setTextures(); geometry not ready"; } } -void NetworkGeometry::textureLoaded(const QWeakPointer& networkTexture) { - numTextureLoaded++; +bool Geometry::areTexturesLoaded() const { + if (!_areTexturesLoaded) { + _hasTransparentTextures = false; + + for (auto& material : _materials) { + // Check if material textures are loaded + if (std::any_of(material->_textures.cbegin(), material->_textures.cend(), + [](const NetworkMaterial::Textures::value_type& it) { return it.texture && !it.texture->isLoaded(); })) { + + return false; + } + + // If material textures are loaded, check the material translucency + const auto albedoTexture = material->_textures[NetworkMaterial::MapChannel::ALBEDO_MAP]; + if (albedoTexture.texture && albedoTexture.texture->getGPUTexture()) { + material->resetOpacityMap(); + + _hasTransparentTextures |= material->getKey().isTranslucent(); + } + } + + _areTexturesLoaded = true; + } + return true; } + +const std::shared_ptr Geometry::getShapeMaterial(int shapeID) const { + if ((shapeID >= 0) && (shapeID < (int)_shapes->size())) { + int materialID = _shapes->at(shapeID).materialID; + if ((materialID >= 0) && (materialID < (int)_materials.size())) { + return _materials[materialID]; + } + } + return nullptr; +} + +NetworkGeometry::NetworkGeometry(const GeometryResource::Pointer& networkGeometry) : _resource(networkGeometry) { + connect(_resource.data(), &Resource::finished, this, &NetworkGeometry::resourceFinished); + connect(_resource.data(), &Resource::onRefresh, this, &NetworkGeometry::resourceRefreshed); + if (_resource->isLoaded()) { + resourceFinished(); + } +} + +void NetworkGeometry::resourceFinished() { + _instance = std::make_shared(*_resource); +} + +void NetworkGeometry::resourceRefreshed() { + _instance.reset(); +} + +const QString NetworkMaterial::NO_TEXTURE = QString(); + +const QString& NetworkMaterial::getTextureName(MapChannel channel) { + if (_textures[channel].texture) { + return _textures[channel].name; + } + return NO_TEXTURE; +} + +QUrl NetworkMaterial::getTextureUrl(const QUrl& url, const FBXTexture& texture) { + // If content is inline, cache it under the fbx file, not its url + const auto baseUrl = texture.content.isEmpty() ? url: QUrl(url.url() + "/"); + return baseUrl.resolved(QUrl(texture.filename)); +} + +model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, + TextureType type, MapChannel channel) { + const auto url = getTextureUrl(baseUrl, fbxTexture); + const auto texture = DependencyManager::get()->getTexture(url, type, fbxTexture.content); + _textures[channel] = Texture { fbxTexture.name, texture }; + + auto map = std::make_shared(); + map->setTextureSource(texture->_textureSource); + return map; +} + +model::TextureMapPointer NetworkMaterial::fetchTextureMap(const QUrl& url, TextureType type, MapChannel channel) { + const auto texture = DependencyManager::get()->getTexture(url, type); + _textures[channel].texture = texture; + + auto map = std::make_shared(); + map->setTextureSource(texture->_textureSource); + return map; +} + +NetworkMaterial::NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl) { + _textures = Textures(MapChannel::NUM_MAP_CHANNELS); + if (!material.albedoTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.albedoTexture, DEFAULT_TEXTURE, MapChannel::ALBEDO_MAP); + map->setTextureTransform(material.albedoTexture.transform); + + if (!material.opacityTexture.filename.isEmpty()) { + if (material.albedoTexture.filename == material.opacityTexture.filename) { + // Best case scenario, just indicating that the albedo map contains transparency + // TODO: Different albedo/opacity maps are not currently supported + map->setUseAlphaChannel(true); + } + } + + setTextureMap(MapChannel::ALBEDO_MAP, map); + } + + + if (!material.normalTexture.filename.isEmpty()) { + auto type = (material.normalTexture.isBumpmap ? BUMP_TEXTURE : NORMAL_TEXTURE); + auto map = fetchTextureMap(textureBaseUrl, material.normalTexture, type, MapChannel::NORMAL_MAP); + setTextureMap(MapChannel::NORMAL_MAP, map); + } + + if (!material.roughnessTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.roughnessTexture, ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } else if (!material.glossTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.glossTexture, GLOSS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } + + if (!material.metallicTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.metallicTexture, METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } else if (!material.specularTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.specularTexture, SPECULAR_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } + + if (!material.occlusionTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.occlusionTexture, OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } + + if (!material.emissiveTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.emissiveTexture, EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } + + if (!material.lightmapTexture.filename.isEmpty()) { + auto map = fetchTextureMap(textureBaseUrl, material.lightmapTexture, LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + map->setTextureTransform(material.lightmapTexture.transform); + map->setLightmapOffsetScale(material.lightmapParams.x, material.lightmapParams.y); + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +NetworkMaterial::NetworkMaterial(const NetworkMaterial& material, const QVariantMap& textureMap) : NetworkMaterial(material) { + _textures = material._textures; + + const auto& albedoName = getTextureName(MapChannel::ALBEDO_MAP); + const auto& normalName = getTextureName(MapChannel::NORMAL_MAP); + const auto& roughnessName = getTextureName(MapChannel::ROUGHNESS_MAP); + const auto& metallicName = getTextureName(MapChannel::METALLIC_MAP); + const auto& occlusionName = getTextureName(MapChannel::OCCLUSION_MAP); + const auto& emissiveName = getTextureName(MapChannel::EMISSIVE_MAP); + const auto& lightmapName = getTextureName(MapChannel::LIGHTMAP_MAP); + + if (!albedoName.isEmpty() && textureMap.contains(albedoName)) { + auto map = fetchTextureMap(textureMap[albedoName].toUrl(), DEFAULT_TEXTURE, MapChannel::ALBEDO_MAP); + map->setTextureTransform(getTextureMap(MapChannel::ALBEDO_MAP)->getTextureTransform()); + // when reassigning the albedo texture we also check for the alpha channel used as opacity + map->setUseAlphaChannel(true); + setTextureMap(MapChannel::ALBEDO_MAP, map); + } + + if (!normalName.isEmpty() && textureMap.contains(normalName)) { + auto map = fetchTextureMap(textureMap[normalName].toUrl(), DEFAULT_TEXTURE, MapChannel::NORMAL_MAP); + setTextureMap(MapChannel::NORMAL_MAP, map); + } + + if (!roughnessName.isEmpty() && textureMap.contains(roughnessName)) { + // FIXME: If passing a gloss map instead of a roughmap how do we know? + auto map = fetchTextureMap(textureMap[roughnessName].toUrl(), ROUGHNESS_TEXTURE, MapChannel::ROUGHNESS_MAP); + setTextureMap(MapChannel::ROUGHNESS_MAP, map); + } + + if (!metallicName.isEmpty() && textureMap.contains(metallicName)) { + // FIXME: If passing a specular map instead of a metallic how do we know? + auto map = fetchTextureMap(textureMap[metallicName].toUrl(), METALLIC_TEXTURE, MapChannel::METALLIC_MAP); + setTextureMap(MapChannel::METALLIC_MAP, map); + } + + if (!occlusionName.isEmpty() && textureMap.contains(occlusionName)) { + auto map = fetchTextureMap(textureMap[occlusionName].toUrl(), OCCLUSION_TEXTURE, MapChannel::OCCLUSION_MAP); + setTextureMap(MapChannel::OCCLUSION_MAP, map); + } + + if (!emissiveName.isEmpty() && textureMap.contains(emissiveName)) { + auto map = fetchTextureMap(textureMap[emissiveName].toUrl(), EMISSIVE_TEXTURE, MapChannel::EMISSIVE_MAP); + setTextureMap(MapChannel::EMISSIVE_MAP, map); + } + + if (!lightmapName.isEmpty() && textureMap.contains(lightmapName)) { + auto map = fetchTextureMap(textureMap[lightmapName].toUrl(), LIGHTMAP_TEXTURE, MapChannel::LIGHTMAP_MAP); + auto oldMap = getTextureMap(MapChannel::LIGHTMAP_MAP); + map->setTextureTransform(oldMap->getTextureTransform()); + glm::vec2 offsetScale = oldMap->getLightmapOffsetScale(); + map->setLightmapOffsetScale(offsetScale.x, offsetScale.y); + setTextureMap(MapChannel::LIGHTMAP_MAP, map); + } +} + +#include "ModelCache.moc" + diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 1c76a0b878..a2fcc9d741 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -1,9 +1,9 @@ // // ModelCache.h -// libraries/model-networking/src/model-networking +// libraries/model-networking // -// Created by Sam Gateau on 9/21/15. -// Copyright 2013 High Fidelity, Inc. +// Created by Zach Pomerantz on 3/15/16. +// Copyright 2016 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -12,200 +12,156 @@ #ifndef hifi_ModelCache_h #define hifi_ModelCache_h -#include -#include - #include #include -#include "FBXReader.h" -#include "OBJReader.h" - -#include -#include - - #include #include -class NetworkGeometry; -class NetworkMesh; +#include "FBXReader.h" +#include "TextureCache.h" + +// Alias instead of derive to avoid copying +using NetworkMesh = model::Mesh; + class NetworkTexture; class NetworkMaterial; class NetworkShape; +class NetworkGeometry; -/// Stores cached geometry. +class GeometryMappingResource; + +/// Stores cached model geometries. class ModelCache : public ResourceCache, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY public: - virtual QSharedPointer createResource(const QUrl& url, const QSharedPointer& fallback, - bool delayLoad, const void* extra); + /// Loads a model geometry from the specified URL. + std::shared_ptr getGeometry(const QUrl& url, + const QVariantHash& mapping = QVariantHash(), const QUrl& textureBaseUrl = QUrl()); - /// Loads geometry from the specified URL. - /// \param fallback a fallback URL to load if the desired one is unavailable - /// \param delayLoad if true, don't load the geometry immediately; wait until load is first requested - QSharedPointer getGeometry(const QUrl& url, const QUrl& fallback = QUrl(), bool delayLoad = false); +protected: + friend class GeometryMappingResource; + + virtual QSharedPointer createResource(const QUrl& url, + const QSharedPointer& fallback, bool delayLoad, const void* extra); private: ModelCache(); - virtual ~ModelCache(); + virtual ~ModelCache() = default; +}; - QHash > _networkGeometry; +class Geometry { +public: + using Pointer = std::shared_ptr; + + // Immutable over lifetime + using NetworkMeshes = std::vector>; + using NetworkShapes = std::vector; + + // Mutable, but must retain structure of vector + using NetworkMaterials = std::vector>; + + const FBXGeometry& getGeometry() const { return *_geometry; } + const NetworkMeshes& getMeshes() const { return *_meshes; } + const std::shared_ptr getShapeMaterial(int shapeID) const; + + const QVariantMap getTextures() const; + void setTextures(const QVariantMap& textureMap); + + virtual bool areTexturesLoaded() const; + // Returns true if any albedo texture has a non-masking alpha channel. + // This can only be known after areTexturesLoaded(). + bool hasTransparentTextures() const { return _hasTransparentTextures; } + +protected: + friend class GeometryMappingResource; + + // Shared across all geometries, constant throughout lifetime + std::shared_ptr _geometry; + std::shared_ptr _meshes; + std::shared_ptr _shapes; + + // Copied to each geometry, mutable throughout lifetime via setTextures + NetworkMaterials _materials; + +private: + mutable bool _areTexturesLoaded { false }; + mutable bool _hasTransparentTextures { false }; +}; + +/// A geometry loaded from the network. +class GeometryResource : public Resource, public Geometry { +public: + using Pointer = QSharedPointer; + + GeometryResource(const QUrl& url) : Resource(url) {} + + virtual bool areTexturesLoaded() const { return isLoaded() && Geometry::areTexturesLoaded(); } + +protected: + virtual bool isCacheable() const override { return _loaded; } }; class NetworkGeometry : public QObject { Q_OBJECT - public: - // mapping is only used if url is a .fbx or .obj file, it is essentially the content of an fst file. - // if delayLoad is true, the url will not be immediately downloaded. - // use the attemptRequest method to initiate the download. - NetworkGeometry(const QUrl& url, bool delayLoad, const QVariantHash& mapping, const QUrl& textureBaseUrl = QUrl()); - ~NetworkGeometry(); + using Pointer = std::shared_ptr; - const QUrl& getURL() const { return _url; } + NetworkGeometry() = delete; + NetworkGeometry(const GeometryResource::Pointer& networkGeometry); - void attemptRequest(); + const QUrl& getURL() { return _resource->getURL(); } - // true when the geometry is loaded (but maybe not it's associated textures) - bool isLoaded() const; + /// Returns the geometry, if it is loaded (must be checked!) + const Geometry::Pointer& getGeometry() { return _instance; } - // true when the requested geometry and its textures are loaded. - bool isLoadedWithTextures() const; +private slots: + void resourceFinished(); + void resourceRefreshed(); - // true if the albedo texture has a non-masking alpha channel. - // This can only be known after isLoadedWithTextures(). - bool hasTransparentTextures() const { return _hasTransparentTextures; } +private: + GeometryResource::Pointer _resource; + Geometry::Pointer _instance { nullptr }; +}; - // WARNING: only valid when isLoaded returns true. - const FBXGeometry& getFBXGeometry() const { return *_geometry; } - const std::vector>& getMeshes() const { return _meshes; } - // const model::AssetPointer getAsset() const { return _asset; } - - // model::MeshPointer getShapeMesh(int shapeID); - // int getShapePart(int shapeID); - - // This would be the final verison - // model::MaterialPointer getShapeMaterial(int shapeID); - const NetworkMaterial* getShapeMaterial(int shapeID); - - - void setTextureWithNameToURL(const QString& name, const QUrl& url); - QStringList getTextureNames() const; - - enum Error { - MissingFilenameInMapping = 0, - MappingRequestError, - ModelRequestError, - ModelParseError - }; - -signals: - // Fired when everything has downloaded and parsed successfully. - void onSuccess(NetworkGeometry& networkGeometry, FBXGeometry& fbxGeometry); - - // Fired when something went wrong. - void onFailure(NetworkGeometry& networkGeometry, Error error); - -public slots: - void textureLoaded(const QWeakPointer& networkTexture); - -protected slots: - void mappingRequestDone(const QByteArray& data); - void mappingRequestError(QNetworkReply::NetworkError error); - - void modelRequestDone(const QByteArray& data); - void modelRequestError(QNetworkReply::NetworkError error); - - void modelParseSuccess(FBXGeometry* geometry); - void modelParseError(int error, QString str); +class NetworkMaterial : public model::Material { +public: + using MapChannel = model::Material::MapChannel; + NetworkMaterial(const FBXMaterial& material, const QUrl& textureBaseUrl); + NetworkMaterial(const NetworkMaterial& material, const QVariantMap& textureMap); protected: - void attemptRequestInternal(); - void requestMapping(const QUrl& url); - void requestModel(const QUrl& url); + friend class Geometry; - enum State { DelayState, - RequestMappingState, - RequestModelState, - ParsingModelState, - SuccessState, - ErrorState }; - State _state; + class Texture { + public: + QString name; + QSharedPointer texture; + }; + using Textures = std::vector; - QUrl _url; - QUrl _mappingUrl; - QUrl _modelUrl; - QVariantHash _mapping; - QUrl _textureBaseUrl; - int numTextureLoaded = 0; + Textures _textures; - Resource* _resource = nullptr; - std::unique_ptr _geometry; // This should go away evenutally once we can put everything we need in the model::AssetPointer - std::vector> _meshes; - std::vector> _materials; - std::vector> _shapes; + static const QString NO_TEXTURE; + const QString& getTextureName(MapChannel channel); - - // The model asset created from this NetworkGeometry - // model::AssetPointer _asset; - - // cache for isLoadedWithTextures() - mutable bool _isLoadedWithTextures = false; - mutable bool _hasTransparentTextures = false; -}; - -/// Reads geometry in a worker thread. -class GeometryReader : public QObject, public QRunnable { - Q_OBJECT -public: - GeometryReader(const QUrl& url, const QByteArray& data, const QVariantHash& mapping); - virtual void run(); -signals: - void onSuccess(FBXGeometry* geometry); - void onError(int error, QString str); private: - QUrl _url; - QByteArray _data; - QVariantHash _mapping; + // Helpers for the ctors + QUrl getTextureUrl(const QUrl& baseUrl, const FBXTexture& fbxTexture); + model::TextureMapPointer fetchTextureMap(const QUrl& baseUrl, const FBXTexture& fbxTexture, + TextureType type, MapChannel channel); + model::TextureMapPointer fetchTextureMap(const QUrl& url, TextureType type, MapChannel channel); }; - class NetworkShape { public: - int _meshID{ -1 }; - int _partID{ -1 }; - int _materialID{ -1 }; + NetworkShape(int mesh, int part, int material) : meshID { mesh }, partID { part }, materialID { material } {} + int meshID { -1 }; + int partID { -1 }; + int materialID { -1 }; }; -class NetworkMaterial { -public: - - model::MaterialPointer _material; - QString emissiveTextureName; - QSharedPointer emissiveTexture; - QString albedoTextureName; - QSharedPointer albedoTexture; - QString normalTextureName; - QSharedPointer normalTexture; - QString roughnessTextureName; - QSharedPointer roughnessTexture; - QString metallicTextureName; - QSharedPointer metallicTexture; - QString occlusionTextureName; - QSharedPointer occlusionTexture; - QString lightmapTextureName; - QSharedPointer lightmapTexture; -}; - - -/// The state associated with a single mesh. -class NetworkMesh { -public: - model::MeshPointer _mesh; -}; - -#endif // hifi_GeometryCache_h +#endif // hifi_ModelCache_h