diff --git a/CesiumGltf/include/CesiumGltf/Model.h b/CesiumGltf/include/CesiumGltf/Model.h index fd379f389..e8a4642fb 100644 --- a/CesiumGltf/include/CesiumGltf/Model.h +++ b/CesiumGltf/include/CesiumGltf/Model.h @@ -1,7 +1,8 @@ #pragma once -#include "CesiumGltf/Library.h" -#include "CesiumGltf/ModelSpec.h" +#include +#include +#include #include @@ -22,7 +23,7 @@ struct CESIUMGLTF_API Model : public ModelSpec { * * @param rhs The model to merge into this one. */ - void merge(Model&& rhs); + CesiumUtility::ErrorList merge(Model&& rhs); /** * @brief A callback function for {@link forEachPrimitiveInScene}. diff --git a/CesiumGltf/src/Model.cpp b/CesiumGltf/src/Model.cpp index 42ee27c3b..ef138804a 100644 --- a/CesiumGltf/src/Model.cpp +++ b/CesiumGltf/src/Model.cpp @@ -1,7 +1,10 @@ #include "CesiumGltf/Model.h" #include "CesiumGltf/AccessorView.h" +#include "CesiumGltf/ExtensionExtMeshFeatures.h" #include "CesiumGltf/ExtensionKhrDracoMeshCompression.h" +#include "CesiumGltf/ExtensionMeshPrimitiveExtStructuralMetadata.h" +#include "CesiumGltf/ExtensionModelExtStructuralMetadata.h" #include #include @@ -9,9 +12,14 @@ #include #include +#include + +using namespace CesiumUtility; namespace CesiumGltf { + namespace { + template size_t copyElements(std::vector& to, std::vector& from) { const size_t out = to.size(); @@ -29,9 +37,24 @@ void updateIndex(int32_t& index, size_t offset) noexcept { } index += int32_t(offset); } + +void updateIndex(int64_t& index, size_t offset) noexcept { + if (index == -1) { + return; + } + index += int64_t(offset); +} + +void mergeSchemas( + Schema& lhs, + Schema& rhs, + std::map& classNameMap); + } // namespace -void Model::merge(Model&& rhs) { +ErrorList Model::merge(Model&& rhs) { + ErrorList result; + // TODO: we could generate this pretty easily if the glTF JSON schema made // it clear which index properties refer to which types of objects. @@ -65,6 +88,83 @@ void Model::merge(Model&& rhs) { const size_t firstSkin = copyElements(this->skins, rhs.skins); const size_t firstTexture = copyElements(this->textures, rhs.textures); + size_t firstPropertyTable = 0; + size_t firstPropertyTexture = 0; + size_t firstPropertyAttribute = 0; + + ExtensionModelExtStructuralMetadata* pRhsMetadata = + rhs.getExtension(); + if (pRhsMetadata) { + ExtensionModelExtStructuralMetadata& metadata = + this->addExtension(); + + if (metadata.schemaUri && pRhsMetadata->schemaUri && + *metadata.schemaUri != *pRhsMetadata->schemaUri) { + // We can't merge schema URIs. So the thing to do here is download both + // schemas and merge them. But for now we're just reporting an error. + result.emplaceError("Cannot merge EXT_structural_metadata extensions " + "with different schemaUris."); + } else if (pRhsMetadata->schemaUri) { + metadata.schemaUri = pRhsMetadata->schemaUri; + } + + std::map classNameMap; + + if (metadata.schema && pRhsMetadata->schema) { + mergeSchemas(*metadata.schema, *pRhsMetadata->schema, classNameMap); + } else if (pRhsMetadata->schema) { + metadata.schema = std::move(pRhsMetadata->schema); + } + + firstPropertyTable = + copyElements(metadata.propertyTables, pRhsMetadata->propertyTables); + firstPropertyTexture = + copyElements(metadata.propertyTextures, pRhsMetadata->propertyTextures); + firstPropertyAttribute = copyElements( + metadata.propertyAttributes, + pRhsMetadata->propertyAttributes); + + for (size_t i = firstPropertyTable; i < metadata.propertyTables.size(); + ++i) { + PropertyTable& propertyTable = metadata.propertyTables[i]; + auto it = classNameMap.find(propertyTable.classProperty); + if (it != classNameMap.end()) { + propertyTable.classProperty = it->second; + } + + for (std::pair& pair : + propertyTable.properties) { + updateIndex(pair.second.values, firstBufferView); + updateIndex(pair.second.arrayOffsets, firstBufferView); + updateIndex(pair.second.stringOffsets, firstBufferView); + } + } + + for (size_t i = firstPropertyTexture; i < metadata.propertyTextures.size(); + ++i) { + PropertyTexture& propertyTexture = metadata.propertyTextures[i]; + auto it = classNameMap.find(propertyTexture.classProperty); + if (it != classNameMap.end()) { + propertyTexture.classProperty = it->second; + } + + for (std::pair& pair : + propertyTexture.properties) { + updateIndex(pair.second.index, firstTexture); + } + } + + for (size_t i = firstPropertyAttribute; + i < metadata.propertyAttributes.size(); + ++i) { + PropertyAttribute& propertyAttribute = metadata.propertyAttributes[i]; + auto it = classNameMap.find(propertyAttribute.classProperty); + if (it != classNameMap.end()) { + propertyAttribute.classProperty = it->second; + } + } + } + // Update the copied indices for (size_t i = firstAccessor; i < this->accessors.size(); ++i) { Accessor& accessor = this->accessors[i]; @@ -122,6 +222,32 @@ void Model::merge(Model&& rhs) { if (pDraco) { updateIndex(pDraco->bufferView, firstBufferView); } + + ExtensionMeshPrimitiveExtStructuralMetadata* pMetadata = + primitive.getExtension(); + if (pMetadata) { + for (int64_t& propertyTextureID : pMetadata->propertyTextures) { + updateIndex(propertyTextureID, firstPropertyTexture); + } + + for (int64_t& propertyAttributeID : pMetadata->propertyAttributes) { + updateIndex(propertyAttributeID, firstPropertyAttribute); + } + } + + ExtensionExtMeshFeatures* pMeshFeatures = + primitive.getExtension(); + if (pMeshFeatures) { + for (FeatureId& featureId : pMeshFeatures->featureIds) { + if (featureId.propertyTable) { + updateIndex(*featureId.propertyTable, firstPropertyTable); + } + + if (featureId.texture) { + updateIndex(featureId.texture->index, firstTexture); + } + } + } } } @@ -215,6 +341,8 @@ void Model::merge(Model&& rhs) { this->scene = int32_t(this->scenes.size() - 1); } + + return result; } namespace { @@ -694,4 +822,85 @@ bool Model::isExtensionRequired( extensionName) != this->extensionsRequired.end(); } +namespace { + +template +std::string findAvailableName( + const std::unordered_map& map, + const std::string& name) { + auto it = map.find(name); + if (it == map.end()) + return name; + + // Name already exists in the map, so find a numbered name that doesn't. + + uint64_t number = 1; + while (number < std::numeric_limits::max()) { + std::string newName = fmt::format("{}_{}", name, number); + it = map.find(newName); + if (it == map.end()) { + return newName; + } + + ++number; + } + + // Realistically, this can't happen. It would mean we checked all 2^64 + // possible names and none of them are available. + assert(false); + return name; +} + +void mergeSchemas( + Schema& lhs, + Schema& rhs, + std::map& classNameMap) { + if (!lhs.name) + lhs.name = rhs.name; + else if (rhs.name && *lhs.name != *rhs.name) + lhs.name.emplace("Merged"); + + if (!lhs.description) + lhs.description = rhs.description; + else if (rhs.description && *lhs.description != *rhs.description) + lhs.description.emplace("This is a merged schema created by combining " + "together the schemas from multiple glTFs."); + + if (!lhs.version) + lhs.version = rhs.version; + else if (rhs.version && *lhs.version != *rhs.version) + lhs.version.reset(); + + std::map enumNameMap; + + for (std::pair& pair : rhs.enums) { + std::string availableName = findAvailableName(lhs.enums, pair.first); + lhs.enums[availableName] = std::move(pair.second); + if (availableName != pair.first) + enumNameMap.emplace(pair.first, availableName); + } + + for (std::pair& pair : rhs.classes) { + std::string availableName = findAvailableName(lhs.classes, pair.first); + Class& klass = lhs.classes[availableName]; + klass = std::move(pair.second); + + if (availableName != pair.first) + classNameMap.emplace(pair.first, availableName); + + // Remap enum names in class properties. + for (std::pair& propertyPair : + klass.properties) { + if (propertyPair.second.enumType) { + auto it = enumNameMap.find(*propertyPair.second.enumType); + if (it != enumNameMap.end()) { + propertyPair.second.enumType = it->second; + } + } + } + } +} + +} // namespace + } // namespace CesiumGltf