diff --git a/CHANGES.md b/CHANGES.md index cbb85218c..13088310c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -40,6 +40,7 @@ - Added `getNodeTransform`, `setNodeTransform`, `removeUnusedTextures`, `removeUnusedSamplers`, `removeUnusedImages`, `removeUnusedAccessors`, `removeUnusedBufferViews`, and `compactBuffers` methods to `GltfUtilities`. - Added `postprocessGltf` method to `GltfReader`. - `Model::merge` now merges the `EXT_structural_metadata` and `EXT_mesh_features` extensions. It also now returns an `ErrorList`, used to report warnings and errors about the merge process. +- Added support for I3dm 3D Tile content files. ##### Fixes :wrench: diff --git a/CMakeLists.txt b/CMakeLists.txt index b799e2aa5..c63d4c575 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,8 +36,8 @@ if (NOT DEFINED GLOB_USE_CONFIGURE_DEPENDS) ) endif() -set(CESIUM_DEBUG_POSTFIX "d") -set(CESIUM_RELEASE_POSTFIX "") +set(CESIUM_DEBUG_POSTFIX "d" CACHE STRING "debug postfix for cesium native") +set(CESIUM_RELEASE_POSTFIX "" CACHE STRING "release postfix for cesium native") set(CMAKE_DEBUG_POSTFIX ${CESIUM_DEBUG_POSTFIX}) set(CMAKE_RELEASE_POSTFIX ${CESIUM_RELEASE_POSTFIX}) @@ -137,8 +137,8 @@ endfunction() add_subdirectory(extern EXCLUDE_FROM_ALL) # These libraries override the debug postfix, so re-override it. -set_target_properties(spdlog PROPERTIES DEBUG_POSTFIX ${CESIUM_DEBUG_POSTFIX}) -set_target_properties(tinyxml2 PROPERTIES DEBUG_POSTFIX ${CESIUM_DEBUG_POSTFIX}) +set_target_properties(spdlog PROPERTIES DEBUG_POSTFIX "${CESIUM_DEBUG_POSTFIX}") +set_target_properties(tinyxml2 PROPERTIES DEBUG_POSTFIX "${CESIUM_DEBUG_POSTFIX}") # Public Targets add_subdirectory(CesiumUtility) diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/B3dmToGltfConverter.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/B3dmToGltfConverter.h index a13e643c4..c21c6368e 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/B3dmToGltfConverter.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/B3dmToGltfConverter.h @@ -2,6 +2,7 @@ #include "GltfConverterResult.h" +#include #include #include @@ -10,9 +11,12 @@ #include namespace Cesium3DTilesContent { +struct AssetFetcher; + struct B3dmToGltfConverter { - static GltfConverterResult convert( + static CesiumAsync::Future convert( const gsl::span& b3dmBinary, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); }; } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/BinaryToGltfConverter.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/BinaryToGltfConverter.h index 910f3c305..ed4459de4 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/BinaryToGltfConverter.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/BinaryToGltfConverter.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -8,13 +9,19 @@ #include namespace Cesium3DTilesContent { +struct AssetFetcher; + struct BinaryToGltfConverter { public: - static GltfConverterResult convert( + static CesiumAsync::Future convert( const gsl::span& gltfBinary, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); private: + static GltfConverterResult convertImmediate( + const gsl::span& gltfBinary, + const CesiumGltfReader::GltfReaderOptions& options); static CesiumGltfReader::GltfReader _gltfReader; }; } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/CmptToGltfConverter.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/CmptToGltfConverter.h index ad7280a32..836d103be 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/CmptToGltfConverter.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/CmptToGltfConverter.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -8,9 +9,12 @@ #include namespace Cesium3DTilesContent { +struct AssetFetcher; + struct CmptToGltfConverter { - static GltfConverterResult convert( + static CesiumAsync::Future convert( const gsl::span& cmptBinary, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); }; } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterResult.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterResult.h index 54fd31114..b32de5244 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterResult.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterResult.h @@ -5,7 +5,12 @@ #include #include +#include +#include + #include +#include +#include namespace Cesium3DTilesContent { /** diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterUtility.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterUtility.h new file mode 100644 index 000000000..2ce646980 --- /dev/null +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverterUtility.h @@ -0,0 +1,132 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace CesiumGltf { +class Model; +class Buffer; +} // namespace CesiumGltf + +namespace Cesium3DTilesContent { + +namespace GltfConverterUtility { +std::optional parseOffsetForSemantic( + const rapidjson::Document& document, + const char* semantic, + CesiumUtility::ErrorList& errorList); + +typedef bool (rapidjson::Value::*ValuePredicate)() const; + +template bool isValue(const rapidjson::Value& value); +template T getValue(const rapidjson::Value& value); + +template +std::optional getOptional(const rapidjson::Value& value) { + if (isValue(value)) { + return std::make_optional(getValue(value)); + } + return {}; +} + +template +std::optional +getValue(const rapidjson::Document& document, const char* semantic) { + const auto valueIt = document.FindMember(semantic); + if (valueIt == document.MemberEnd() || !isValue(valueIt->value)) { + return {}; + } + return std::make_optional(getValue(valueIt->value)); +} + +template <> inline bool isValue(const rapidjson::Value& value) { + return value.IsBool(); +} + +template <> inline bool getValue(const rapidjson::Value& value) { + return value.GetBool(); +} + +template <> inline bool isValue(const rapidjson::Value& value) { + return value.IsUint(); +} + +template <> inline uint32_t getValue(const rapidjson::Value& value) { + return value.GetUint(); +} + +bool validateJsonArrayValues( + const rapidjson::Value& arrayValue, + uint32_t expectedLength, + ValuePredicate predicate); + +std::optional +parseArrayValueDVec3(const rapidjson::Value& arrayValue); + +std::optional +parseArrayValueDVec3(const rapidjson::Document& document, const char* name); + +int32_t +createBufferInGltf(CesiumGltf::Model& gltf, std::vector buffer = {}); + +int32_t createBufferViewInGltf( + CesiumGltf::Model& gltf, + const int32_t bufferId, + const int64_t byteLength, + const int64_t byteStride); + +int32_t createAccessorInGltf( + CesiumGltf::Model& gltf, + const int32_t bufferViewId, + const int32_t componentType, + const int64_t count, + const std::string type); + +/** + * Applies the given relative-to-center (RTC) translation to the transforms of + * all nodes in the glTF. This is useful in converting i3dm files, where the RTC + * translation must be applied to the model before the i3dm instance + * transform. It's also the 3D Tiles 1.1 "way" to do away with RTC and encode it + * directly in the glTF. + */ +void applyRtcToNodes(CesiumGltf::Model& gltf, const glm::dvec3& rtc); + +template +GlmType toGlm(const GLTFType& gltfVal); + +template +GlmType toGlm(const CesiumGltf::AccessorTypes::VEC3& gltfVal) { + return GlmType(gltfVal.value[0], gltfVal.value[1], gltfVal.value[2]); +} + +template +GlmType +toGlmQuat(const CesiumGltf::AccessorTypes::VEC4& gltfVal) { + if constexpr (std::is_same()) { + return GlmType( + gltfVal.value[3], + gltfVal.value[0], + gltfVal.value[1], + gltfVal.value[2]); + } else { + return GlmType( + CesiumGltf::normalize(gltfVal.value[3]), + CesiumGltf::normalize(gltfVal.value[0]), + CesiumGltf::normalize(gltfVal.value[1]), + CesiumGltf::normalize(gltfVal.value[2])); + } +} + +} // namespace GltfConverterUtility +} // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverters.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverters.h index bc07748d1..f988b8535 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverters.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/GltfConverters.h @@ -3,6 +3,8 @@ #include "Library.h" #include +#include +#include #include #include @@ -12,6 +14,39 @@ #include namespace Cesium3DTilesContent { + +struct AssetFetcherResult { + std::vector bytes; + CesiumUtility::ErrorList errorList; +}; + +/** + * Object that makes a recursive request to fetch an asset, mostly for the + * benefit of i3dm files. + */ +struct CESIUM3DTILESCONTENT_API AssetFetcher { + AssetFetcher( + const CesiumAsync::AsyncSystem& asyncSystem_, + const std::shared_ptr& pAssetAccessor_, + const std::string& baseUrl_, + const glm::dmat4 tileTransform_, + const std::vector& requestHeaders_) + : asyncSystem(asyncSystem_), + pAssetAccessor(pAssetAccessor_), + baseUrl(baseUrl_), + tileTransform(tileTransform_), + requestHeaders(requestHeaders_) {} + + CesiumAsync::Future + get(const std::string& relativeUrl) const; + + const CesiumAsync::AsyncSystem& asyncSystem; + const std::shared_ptr pAssetAccessor; + const std::string baseUrl; + glm::dmat4 tileTransform; // For ENU transforms in i3dm + const std::vector& requestHeaders; +}; + /** * @brief Creates {@link GltfConverterResult} objects from a * a binary content. @@ -32,9 +67,10 @@ class CESIUM3DTILESCONTENT_API GltfConverters { * @brief A function pointer that can create a {@link GltfConverterResult} from a * tile binary content. */ - using ConverterFunction = GltfConverterResult (*)( + using ConverterFunction = CesiumAsync::Future (*)( const gsl::span& content, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& subprocessor); /** * @brief Register the given function for the given magic header. @@ -117,13 +153,16 @@ class CESIUM3DTILESCONTENT_API GltfConverters { * the converter. * @param content The tile binary content that may contains the magic header * to look up the converter and is used to convert to gltf model. - * @param options The {@link CesiumGltfReader::GltfReaderOptions} for how to read a glTF. + * @param options The {@link CesiumGltfReader::GltfReaderOptions} for how to + * read a glTF. + * @param assetFetcher An object that can perform recursive asset requests. * @return The {@link GltfConverterResult} that stores the gltf model converted from the binary data. */ - static GltfConverterResult convert( + static CesiumAsync::Future convert( const std::string& filePath, const gsl::span& content, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); /** * @brief Creates the {@link GltfConverterResult} from the given @@ -142,12 +181,15 @@ class CESIUM3DTILESCONTENT_API GltfConverters { * * @param content The tile binary content that may contains the magic header * to look up the converter and is used to convert to gltf model. - * @param options The {@link CesiumGltfReader::GltfReaderOptions} for how to read a glTF. + * @param options The {@link CesiumGltfReader::GltfReaderOptions} for how to + * read a glTF. + * @param assetFetcher An object that can perform recursive asset requests. * @return The {@link GltfConverterResult} that stores the gltf model converted from the binary data. */ - static GltfConverterResult convert( + static CesiumAsync::Future convert( const gsl::span& content, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); private: static std::string toLowerCase(const std::string_view& str); diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/I3dmToGltfConverter.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/I3dmToGltfConverter.h new file mode 100644 index 000000000..f709903ac --- /dev/null +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/I3dmToGltfConverter.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include + +namespace Cesium3DTilesContent { +struct AssetFetcher; + +struct I3dmToGltfConverter { + static CesiumAsync::Future convert( + const gsl::span& instancesBinary, + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); +}; +} // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/include/Cesium3DTilesContent/PntsToGltfConverter.h b/Cesium3DTilesContent/include/Cesium3DTilesContent/PntsToGltfConverter.h index 48593ca00..31a61a200 100644 --- a/Cesium3DTilesContent/include/Cesium3DTilesContent/PntsToGltfConverter.h +++ b/Cesium3DTilesContent/include/Cesium3DTilesContent/PntsToGltfConverter.h @@ -2,6 +2,7 @@ #include "GltfConverterResult.h" +#include #include #include @@ -10,9 +11,12 @@ #include namespace Cesium3DTilesContent { +struct AssetFetcher; + struct PntsToGltfConverter { - static GltfConverterResult convert( + static CesiumAsync::Future convert( const gsl::span& pntsBinary, - const CesiumGltfReader::GltfReaderOptions& options); + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher); }; } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/B3dmToGltfConverter.cpp b/Cesium3DTilesContent/src/B3dmToGltfConverter.cpp index 0bcf9b9d2..0f2c423c9 100644 --- a/Cesium3DTilesContent/src/B3dmToGltfConverter.cpp +++ b/Cesium3DTilesContent/src/B3dmToGltfConverter.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -113,12 +114,12 @@ void parseB3dmHeader( } } -void convertB3dmContentToGltf( +CesiumAsync::Future convertB3dmContentToGltf( const gsl::span& b3dmBinary, const B3dmHeader& header, uint32_t headerLength, const CesiumGltfReader::GltfReaderOptions& options, - GltfConverterResult& result) { + const AssetFetcher& assetFetcher) { const uint32_t glbStart = headerLength + header.featureTableJsonByteLength + header.featureTableBinaryByteLength + header.batchTableJsonByteLength + @@ -126,20 +127,17 @@ void convertB3dmContentToGltf( const uint32_t glbEnd = header.byteLength; if (glbEnd <= glbStart) { + GltfConverterResult result; result.errors.emplaceError( "The B3DM is invalid because the start of the " "glTF model is after the end of the entire B3DM."); - return; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } const gsl::span glbData = b3dmBinary.subspan(glbStart, glbEnd - glbStart); - GltfConverterResult binToGltfResult = - BinaryToGltfConverter::convert(glbData, options); - - result.model = std::move(binToGltfResult.model); - result.errors.merge(std::move(binToGltfResult.errors)); + return BinaryToGltfConverter::convert(glbData, options, assetFetcher); } rapidjson::Document parseFeatureTableJsonData( @@ -231,28 +229,34 @@ void convertB3dmMetadataToGltfStructuralMetadata( } } // namespace -GltfConverterResult B3dmToGltfConverter::convert( +CesiumAsync::Future B3dmToGltfConverter::convert( const gsl::span& b3dmBinary, - const CesiumGltfReader::GltfReaderOptions& options) { + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher) { GltfConverterResult result; B3dmHeader header; uint32_t headerLength = 0; parseB3dmHeader(b3dmBinary, header, headerLength, result); if (result.errors) { - return result; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } - convertB3dmContentToGltf(b3dmBinary, header, headerLength, options, result); - if (result.errors) { - return result; - } - - convertB3dmMetadataToGltfStructuralMetadata( - b3dmBinary, - header, - headerLength, - result); - - return result; + return convertB3dmContentToGltf( + b3dmBinary, + header, + headerLength, + options, + assetFetcher) + .thenImmediately( + [b3dmBinary, header, headerLength](GltfConverterResult&& glbResult) { + if (!glbResult.errors) { + convertB3dmMetadataToGltfStructuralMetadata( + b3dmBinary, + header, + headerLength, + glbResult); + } + return std::move(glbResult); + }); } } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/BinaryToGltfConverter.cpp b/Cesium3DTilesContent/src/BinaryToGltfConverter.cpp index d6420c89b..9a9bcf401 100644 --- a/Cesium3DTilesContent/src/BinaryToGltfConverter.cpp +++ b/Cesium3DTilesContent/src/BinaryToGltfConverter.cpp @@ -1,9 +1,10 @@ #include +#include namespace Cesium3DTilesContent { CesiumGltfReader::GltfReader BinaryToGltfConverter::_gltfReader; -GltfConverterResult BinaryToGltfConverter::convert( +GltfConverterResult BinaryToGltfConverter::convertImmediate( const gsl::span& gltfBinary, const CesiumGltfReader::GltfReaderOptions& options) { CesiumGltfReader::GltfReaderResult loadedGltf = @@ -15,4 +16,12 @@ GltfConverterResult BinaryToGltfConverter::convert( result.errors.warnings = std::move(loadedGltf.warnings); return result; } + +CesiumAsync::Future BinaryToGltfConverter::convert( + const gsl::span& gltfBinary, + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher) { + return assetFetcher.asyncSystem.createResolvedFuture( + convertImmediate(gltfBinary, options)); +} } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/CmptToGltfConverter.cpp b/Cesium3DTilesContent/src/CmptToGltfConverter.cpp index 5676e42e4..ae14d900d 100644 --- a/Cesium3DTilesContent/src/CmptToGltfConverter.cpp +++ b/Cesium3DTilesContent/src/CmptToGltfConverter.cpp @@ -22,13 +22,14 @@ static_assert(sizeof(CmptHeader) == 16); static_assert(sizeof(InnerHeader) == 12); } // namespace -GltfConverterResult CmptToGltfConverter::convert( +CesiumAsync::Future CmptToGltfConverter::convert( const gsl::span& cmptBinary, - const CesiumGltfReader::GltfReaderOptions& options) { + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher) { GltfConverterResult result; if (cmptBinary.size() < sizeof(CmptHeader)) { result.errors.emplaceWarning("Composite tile must be at least 16 bytes."); - return result; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } const CmptHeader* pHeader = @@ -36,14 +37,14 @@ GltfConverterResult CmptToGltfConverter::convert( if (std::string(pHeader->magic, 4) != "cmpt") { result.errors.emplaceWarning( "Composite tile does not have the expected magic vaue 'cmpt'."); - return result; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } if (pHeader->version != 1) { result.errors.emplaceWarning(fmt::format( "Unsupported composite tile version {}.", pHeader->version)); - return result; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } if (pHeader->byteLength > cmptBinary.size()) { @@ -51,10 +52,10 @@ GltfConverterResult CmptToGltfConverter::convert( "Composite tile byteLength is {} but only {} bytes are available.", pHeader->byteLength, cmptBinary.size())); - return result; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } - std::vector innerTiles; + std::vector> innerTiles; uint32_t pos = sizeof(CmptHeader); for (uint32_t i = 0; i < pHeader->tilesLength && pos < pHeader->byteLength; @@ -78,7 +79,8 @@ GltfConverterResult CmptToGltfConverter::convert( pos += pInner->byteLength; - innerTiles.emplace_back(GltfConverters::convert(innerData, options)); + innerTiles.emplace_back( + GltfConverters::convert(innerData, options, assetFetcher)); } uint32_t tilesLength = pHeader->tilesLength; @@ -88,26 +90,26 @@ GltfConverterResult CmptToGltfConverter::convert( "Composite tile does not contain any loadable inner " "tiles."); } - - return result; - } - - if (innerTiles.size() == 1) { - return std::move(innerTiles[0]); - } - - for (size_t i = 0; i < innerTiles.size(); ++i) { - if (innerTiles[i].model) { - if (result.model) { - result.model->merge(std::move(*innerTiles[i].model)); - } else { - result.model = std::move(innerTiles[i].model); - } - } - - result.errors.merge(innerTiles[i].errors); + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } - return result; + return assetFetcher.asyncSystem.all(std::move(innerTiles)) + .thenImmediately([](std::vector&& innerResults) { + if (innerResults.size() == 1) { + return innerResults[0]; + } + GltfConverterResult cmptResult; + for (auto& innerTile : innerResults) { + if (innerTile.model) { + if (cmptResult.model) { + cmptResult.model->merge(std::move(*innerTile.model)); + } else { + cmptResult.model = std::move(innerTile.model); + } + } + cmptResult.errors.merge(innerTile.errors); + } + return cmptResult; + }); } } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/GltfConverterUtility.cpp b/Cesium3DTilesContent/src/GltfConverterUtility.cpp new file mode 100644 index 000000000..f2b180864 --- /dev/null +++ b/Cesium3DTilesContent/src/GltfConverterUtility.cpp @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace Cesium3DTilesContent { +namespace GltfConverterUtility { +using namespace CesiumGltf; + +std::optional parseOffsetForSemantic( + const rapidjson::Document& document, + const char* semantic, + CesiumUtility::ErrorList& errorList) { + const auto semanticIt = document.FindMember(semantic); + if (semanticIt == document.MemberEnd() || !semanticIt->value.IsObject()) { + return {}; + } + const auto byteOffsetIt = semanticIt->value.FindMember("byteOffset"); + if (byteOffsetIt == semanticIt->value.MemberEnd() || + !isValue(byteOffsetIt->value)) { + errorList.emplaceError( + std::string("Error parsing feature table, ") + semantic + + "does not have valid byteOffset."); + return {}; + } + return getValue(byteOffsetIt->value); +} + +bool validateJsonArrayValues( + const rapidjson::Value& arrayValue, + uint32_t expectedLength, + ValuePredicate predicate) { + if (!arrayValue.IsArray()) { + return false; + } + + if (arrayValue.Size() != expectedLength) { + return false; + } + + for (rapidjson::SizeType i = 0; i < expectedLength; i++) { + if (!std::invoke(predicate, arrayValue[i])) { + return false; + } + } + + return true; +} + +std::optional +parseArrayValueDVec3(const rapidjson::Value& arrayValue) { + if (validateJsonArrayValues(arrayValue, 3, &rapidjson::Value::IsNumber)) { + return std::make_optional(glm::dvec3( + arrayValue[0].GetDouble(), + arrayValue[1].GetDouble(), + arrayValue[2].GetDouble())); + } + return {}; +} + +std::optional +parseArrayValueDVec3(const rapidjson::Document& document, const char* name) { + const auto arrayIt = document.FindMember(name); + if (arrayIt != document.MemberEnd()) { + return parseArrayValueDVec3(arrayIt->value); + } + return {}; +} + +int32_t createBufferInGltf(Model& gltf, std::vector buffer) { + size_t bufferId = gltf.buffers.size(); + Buffer& gltfBuffer = gltf.buffers.emplace_back(); + gltfBuffer.byteLength = static_cast(buffer.size()); + gltfBuffer.cesium.data = std::move(buffer); + return static_cast(bufferId); +} + +int32_t createBufferViewInGltf( + Model& gltf, + const int32_t bufferId, + const int64_t byteLength, + const int64_t byteStride) { + size_t bufferViewId = gltf.bufferViews.size(); + BufferView& bufferView = gltf.bufferViews.emplace_back(); + bufferView.buffer = bufferId; + bufferView.byteLength = byteLength; + bufferView.byteOffset = 0; + bufferView.byteStride = byteStride; + bufferView.target = BufferView::Target::ARRAY_BUFFER; + + return static_cast(bufferViewId); +} + +int32_t createAccessorInGltf( + Model& gltf, + const int32_t bufferViewId, + const int32_t componentType, + const int64_t count, + const std::string type) { + size_t accessorId = gltf.accessors.size(); + Accessor& accessor = gltf.accessors.emplace_back(); + accessor.bufferView = bufferViewId; + accessor.byteOffset = 0; + accessor.componentType = componentType; + accessor.count = count; + accessor.type = type; + + return static_cast(accessorId); +} + +void applyRtcToNodes(Model& gltf, const glm::dvec3& rtc) { + using namespace CesiumGltfContent; + auto upToZ = GltfUtilities::applyGltfUpAxisTransform(gltf, glm::dmat4x4(1.0)); + auto rtcTransform = inverse(upToZ); + rtcTransform = translate(rtcTransform, rtc); + rtcTransform = rtcTransform * upToZ; + gltf.forEachRootNodeInScene(-1, [&](Model&, Node& node) { + auto nodeTransform = GltfUtilities::getNodeTransform(node); + if (nodeTransform) { + nodeTransform = rtcTransform * *nodeTransform; + GltfUtilities::setNodeTransform(node, *nodeTransform); + } + }); +} + +} // namespace GltfConverterUtility +} // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/GltfConverters.cpp b/Cesium3DTilesContent/src/GltfConverters.cpp index bd0b9b9cb..1940966ee 100644 --- a/Cesium3DTilesContent/src/GltfConverters.cpp +++ b/Cesium3DTilesContent/src/GltfConverters.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include @@ -37,20 +39,21 @@ GltfConverters::getConverterByMagic(const gsl::span& content) { return getConverterByMagic(content, magic); } -GltfConverterResult GltfConverters::convert( +CesiumAsync::Future GltfConverters::convert( const std::string& filePath, const gsl::span& content, - const CesiumGltfReader::GltfReaderOptions& options) { + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher) { std::string magic; auto converterFun = getConverterByMagic(content, magic); if (converterFun) { - return converterFun(content, options); + return converterFun(content, options, assetFetcher); } std::string fileExtension; converterFun = getConverterByFileExtension(filePath, fileExtension); if (converterFun) { - return converterFun(content, options); + return converterFun(content, options, assetFetcher); } ErrorList errors; @@ -60,16 +63,18 @@ GltfConverterResult GltfConverters::convert( fileExtension, magic)); - return GltfConverterResult{std::nullopt, std::move(errors)}; + return assetFetcher.asyncSystem.createResolvedFuture( + GltfConverterResult{std::nullopt, std::move(errors)}); } -GltfConverterResult GltfConverters::convert( +CesiumAsync::Future GltfConverters::convert( const gsl::span& content, - const CesiumGltfReader::GltfReaderOptions& options) { + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher) { std::string magic; auto converter = getConverterByMagic(content, magic); if (converter) { - return converter(content, options); + return converter(content, options, assetFetcher); } ErrorList errors; @@ -77,7 +82,8 @@ GltfConverterResult GltfConverters::convert( "No loader registered for tile with magic value '{}'", magic)); - return GltfConverterResult{std::nullopt, std::move(errors)}; + return assetFetcher.asyncSystem.createResolvedFuture( + GltfConverterResult{std::nullopt, std::move(errors)}); } std::string GltfConverters::toLowerCase(const std::string_view& str) { @@ -128,4 +134,41 @@ GltfConverters::ConverterFunction GltfConverters::getConverterByMagic( return nullptr; } + +CesiumAsync::Future +AssetFetcher::get(const std::string& relativeUrl) const { + auto resolvedUrl = Uri::resolve(baseUrl, relativeUrl); + return pAssetAccessor->get(asyncSystem, resolvedUrl, requestHeaders) + .thenImmediately( + [asyncSystem = asyncSystem]( + std::shared_ptr&& pCompletedRequest) { + const CesiumAsync::IAssetResponse* pResponse = + pCompletedRequest->response(); + AssetFetcherResult assetFetcherResult; + const auto& url = pCompletedRequest->url(); + if (!pResponse) { + assetFetcherResult.errorList.emplaceError(fmt::format( + "Did not receive a valid response for asset {}", + url)); + return asyncSystem.createResolvedFuture( + std::move(assetFetcherResult)); + } + uint16_t statusCode = pResponse->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + assetFetcherResult.errorList.emplaceError(fmt::format( + "Received status code {} for asset {}", + statusCode, + url)); + return asyncSystem.createResolvedFuture( + std::move(assetFetcherResult)); + } + gsl::span asset = pResponse->data(); + std::copy( + asset.begin(), + asset.end(), + std::back_inserter(assetFetcherResult.bytes)); + return asyncSystem.createResolvedFuture( + std::move(assetFetcherResult)); + }); +} } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/I3dmToGltfConverter.cpp b/Cesium3DTilesContent/src/I3dmToGltfConverter.cpp new file mode 100644 index 000000000..2ccab8837 --- /dev/null +++ b/Cesium3DTilesContent/src/I3dmToGltfConverter.cpp @@ -0,0 +1,793 @@ +// Heavily inspired by PntsToGltfConverter.cpp + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using namespace CesiumGltf; + +namespace Cesium3DTilesContent { +using namespace GltfConverterUtility; + +namespace { +struct I3dmHeader { + unsigned char magic[4] = {0, 0, 0, 0}; + uint32_t version = 0; + uint32_t byteLength = 0; + uint32_t featureTableJsonByteLength = 0; + uint32_t featureTableBinaryByteLength = 0; + uint32_t batchTableJsonByteLength = 0; + uint32_t batchTableBinaryByteLength = 0; + uint32_t gltfFormat = 0; +}; + +struct DecodedInstances { + std::vector positions; + std::vector rotations; + std::vector scales; + std::string gltfUri; + bool rotationENU = false; + std::optional rtcCenter; +}; + +// Instance positions may arrive in ECEF coordinates or with other large +// displacements that will cause problems during rendering. Determine the mean +// position of the instances and reposition them relative to it, thus creating +// a new RTC center. +// +// If an RTC center value is already present, then the newly-computed center is +// added to it. + +void repositionInstances(DecodedInstances& decodedInstances) { + if (decodedInstances.positions.empty()) { + return; + } + glm::dvec3 newCenter(0.0, 0.0, 0.0); + for (const auto& pos : decodedInstances.positions) { + newCenter += glm::dvec3(pos); + } + newCenter /= static_cast(decodedInstances.positions.size()); + std::transform( + decodedInstances.positions.begin(), + decodedInstances.positions.end(), + decodedInstances.positions.begin(), + [&](const glm::vec3& pos) { + return glm::vec3(glm::dvec3(pos) - newCenter); + }); + if (decodedInstances.rtcCenter) { + newCenter += *decodedInstances.rtcCenter; + } + decodedInstances.rtcCenter = newCenter; +} + +void parseI3dmHeader( + const gsl::span& instancesBinary, + I3dmHeader& header, + uint32_t& headerLength, + GltfConverterResult& result) { + if (instancesBinary.size() < sizeof(I3dmHeader)) { + result.errors.emplaceError("The I3DM is invalid because it is too small to " + "include a I3DM header."); + return; + } + + const I3dmHeader* pHeader = + reinterpret_cast(instancesBinary.data()); + + header = *pHeader; + headerLength = sizeof(I3dmHeader); + + if (pHeader->version != 1) { + result.errors.emplaceError(fmt::format( + "The I3DM file is version {}, which is unsupported.", + pHeader->version)); + return; + } + + if (static_cast(instancesBinary.size()) < pHeader->byteLength) { + result.errors.emplaceError( + "The I3DM is invalid because the total data available is less than the " + "size specified in its header."); + return; + } +} + +struct I3dmContent { + uint32_t instancesLength = 0; + std::optional rtcCenter; + std::optional quantizedVolumeOffset; + std::optional quantizedVolumeScale; + bool eastNorthUp = false; + + // Offsets into the feature table. + std::optional position; + std::optional positionQuantized; + std::optional normalUp; + std::optional normalRight; + std::optional normalUpOct32p; + std::optional normalRightOct32p; + std::optional scale; + std::optional scaleNonUniform; + std::optional batchId; + CesiumUtility::ErrorList errors; +}; + +glm::vec3 decodeOct32P(const uint16_t rawOct[2]) { + glm::dvec3 result = CesiumUtility::AttributeCompression::octDecodeInRange( + rawOct[0], + rawOct[1], + static_cast(65535)); + return glm::vec3(result); +} + +/* + Calculate the rotation quaternion described by the up, right vectors passed + in NORMAL_UP and NORMAL_RIGHT. This is composed of two rotations: + + The rotation that takes the up vector to its new position; + + The rotation around the new up vector that takes the right vector to its + new position. + + I like to think of each rotation as describing a coordinate frame. The + calculation of the second rotation must take place within the first frame. + + The rotations are calculated by finding the rotation that takes one vector to + another. + */ + +glm::quat rotationFromUpRight(const glm::vec3& up, const glm::vec3& right) { + // First rotation: up + auto upRot = rotation(glm::vec3(0.0f, 1.0f, 0.0f), up); + // We can rotate a point vector by a quaternion using q * (0, v) * + // conj(q). But here we are doing an inverse rotation of the right vector into + // the "up frame." + glm::quat temp = conjugate(upRot) * glm::quat(0.0f, right) * upRot; + glm::vec3 innerRight(temp.x, temp.y, temp.z); + glm::quat rightRot = rotation(glm::vec3(1.0f, 0.0f, 0.0f), innerRight); + return upRot * rightRot; +} + +struct ConvertedI3dm { + GltfConverterResult gltfResult; + DecodedInstances decodedInstances; +}; + +/* The approach: + + Parse the i3dm header, decoding and creating all the instance transforms. + This includes "exotic" things like OCT encoding of rotations and ENU rotations + for each instance. + + For each node with a mesh (a "mesh node"), the instance transforms must be + transformed into the local coordinates of the mesh and then stored in the + EXT_mesh_gpu_instancing extension for that node. It would be nice to avoid a + lot of duplicate data for mesh nodes with the same chain of transforms to + the tile root. One way to do this would be to store the accessors in a hash + table, hashed by mesh transform. + + Add the instance transforms to the glTF buffers, buffer views, and + accessors. + + Future work: Metadata / feature id? +*/ + +std::optional parseI3dmJson( + const gsl::span featureTableJsonData, + CesiumUtility::ErrorList& errors) { + rapidjson::Document featureTableJson; + featureTableJson.Parse( + reinterpret_cast(featureTableJsonData.data()), + featureTableJsonData.size()); + if (featureTableJson.HasParseError()) { + errors.emplaceError(fmt::format( + "Error when parsing feature table JSON, error code {} at byte offset " + "{}", + featureTableJson.GetParseError(), + featureTableJson.GetErrorOffset())); + return {}; + } + I3dmContent parsedContent; + // Global semantics + if (std::optional optInstancesLength = + getValue(featureTableJson, "INSTANCES_LENGTH")) { + parsedContent.instancesLength = *optInstancesLength; + } else { + errors.emplaceError("Error parsing I3DM feature table, no valid " + "INSTANCES_LENGTH was found."); + return {}; + } + parsedContent.rtcCenter = + parseArrayValueDVec3(featureTableJson, "RTC_CENTER"); + parsedContent.position = + parseOffsetForSemantic(featureTableJson, "POSITION", errors); + parsedContent.positionQuantized = + parseOffsetForSemantic(featureTableJson, "POSITION_QUANTIZED", errors); + if (errors.hasErrors()) { + return {}; + } + // I would have liked to just test !parsedContent.position, but the perfectly + // reasonable value of 0 causes the test to be false! + if (!(parsedContent.position.has_value() || + parsedContent.positionQuantized.has_value())) { + errors.emplaceError("I3dm file contains neither POSITION nor " + "POSITION_QUANTIZED semantics."); + return {}; + } + if (parsedContent.positionQuantized.has_value()) { + parsedContent.quantizedVolumeOffset = + parseArrayValueDVec3(featureTableJson, "QUANTIZED_VOLUME_OFFSET"); + if (!parsedContent.quantizedVolumeOffset.has_value()) { + errors.emplaceError( + "Error parsing I3DM feature table, the I3dm uses quantized positions " + "but has no valid QUANTIZED_VOLUME_OFFSET property"); + return {}; + } + parsedContent.quantizedVolumeScale = + parseArrayValueDVec3(featureTableJson, "QUANTIZED_VOLUME_SCALE"); + if (!parsedContent.quantizedVolumeScale.has_value()) { + errors.emplaceError( + "Error parsing I3DM feature table, the I3dm uses quantized positions " + "but has no valid QUANTIZED_VOLUME_SCALE property"); + return {}; + } + } + if (std::optional optENU = + getValue(featureTableJson, "EAST_NORTH_UP")) { + parsedContent.eastNorthUp = *optENU; + } + parsedContent.normalUp = + parseOffsetForSemantic(featureTableJson, "NORMAL_UP", errors); + parsedContent.normalRight = + parseOffsetForSemantic(featureTableJson, "NORMAL_RIGHT", errors); + if (errors.hasErrors()) { + return {}; + } + if (parsedContent.normalUp.has_value() && + !parsedContent.normalRight.has_value()) { + errors.emplaceError("I3dm has NORMAL_UP semantic without NORMAL_RIGHT."); + return {}; + } + if (!parsedContent.normalUp.has_value() && + parsedContent.normalRight.has_value()) { + errors.emplaceError("I3dm has NORMAL_RIGHT semantic without NORMAL_UP."); + return {}; + } + parsedContent.normalUpOct32p = + parseOffsetForSemantic(featureTableJson, "NORMAL_UP_OCT32P", errors); + parsedContent.normalRightOct32p = + parseOffsetForSemantic(featureTableJson, "NORMAL_RIGHT_OCT32P", errors); + if (errors.hasErrors()) { + return {}; + } + if (parsedContent.normalUpOct32p.has_value() && + !parsedContent.normalRightOct32p.has_value()) { + errors.emplaceError( + "I3dm has NORMAL_UP_OCT32P semantic without NORMAL_RIGHT_OCT32P."); + return {}; + } + if (!parsedContent.normalUpOct32p.has_value() && + parsedContent.normalRightOct32p.has_value()) { + errors.emplaceError( + "I3dm has NORMAL_RIGHT_OCT32P semantic without NORMAL_UP_OCT32P."); + return {}; + } + parsedContent.scale = + parseOffsetForSemantic(featureTableJson, "SCALE", errors); + parsedContent.scaleNonUniform = + parseOffsetForSemantic(featureTableJson, "SCALE_NON_UNIFORM", errors); + parsedContent.batchId = + parseOffsetForSemantic(featureTableJson, "BATCH_ID", errors); + if (errors.hasErrors()) { + return {}; + } + return parsedContent; +} + +CesiumAsync::Future convertI3dmContent( + const gsl::span& instancesBinary, + const I3dmHeader& header, + uint32_t headerLength, + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher, + ConvertedI3dm& convertedI3dm) { + DecodedInstances& decodedInstances = convertedI3dm.decodedInstances; + auto finishEarly = [&]() { + return assetFetcher.asyncSystem.createResolvedFuture( + std::move(convertedI3dm)); + }; + if (header.featureTableJsonByteLength == 0 || + header.featureTableBinaryByteLength == 0) { + return finishEarly(); + } + const uint32_t gltfStart = headerLength + header.featureTableJsonByteLength + + header.featureTableBinaryByteLength + + header.batchTableJsonByteLength + + header.batchTableBinaryByteLength; + const uint32_t gltfEnd = header.byteLength; + auto gltfData = instancesBinary.subspan(gltfStart, gltfEnd - gltfStart); + std::optional> assetFuture; + auto featureTableJsonData = + instancesBinary.subspan(headerLength, header.featureTableJsonByteLength); + std::optional parsedJsonResult = + parseI3dmJson(featureTableJsonData, convertedI3dm.gltfResult.errors); + if (!parsedJsonResult) { + finishEarly(); + } + const I3dmContent& parsedContent = *parsedJsonResult; + decodedInstances.rtcCenter = parsedContent.rtcCenter; + decodedInstances.rotationENU = parsedContent.eastNorthUp; + + auto featureTableBinaryData = instancesBinary.subspan( + headerLength + header.featureTableJsonByteLength, + header.featureTableBinaryByteLength); + const std::byte* const pBinaryData = featureTableBinaryData.data(); + const uint32_t numInstances = parsedContent.instancesLength; + decodedInstances.positions.resize(numInstances, glm::vec3(0.0f, 0.0f, 0.0f)); + if (parsedContent.position.has_value()) { + gsl::span rawPositions( + reinterpret_cast( + pBinaryData + *parsedContent.position), + numInstances); + decodedInstances.positions.assign(rawPositions.begin(), rawPositions.end()); + } else { + gsl::span rawQuantizedPositions( + reinterpret_cast( + pBinaryData + *parsedContent.positionQuantized), + numInstances); + std::transform( + rawQuantizedPositions.begin(), + rawQuantizedPositions.end(), + decodedInstances.positions.begin(), + [&parsedContent](auto&& posQuantized) { + glm::vec3 position; + for (unsigned j = 0; j < 3; ++j) { + position[j] = static_cast( + posQuantized[j] / 65535.0 * + (*parsedContent.quantizedVolumeScale)[j] + + (*parsedContent.quantizedVolumeOffset)[j]); + } + return position; + }); + } + decodedInstances.rotations.resize( + numInstances, + glm::quat(1.0f, 0.0f, 0.0f, 0.0f)); + if (parsedContent.normalUp.has_value() && + parsedContent.normalRight.has_value()) { + gsl::span rawUp( + reinterpret_cast( + pBinaryData + *parsedContent.normalUp), + numInstances); + gsl::span rawRight( + reinterpret_cast( + pBinaryData + *parsedContent.normalRight), + numInstances); + std::transform( + rawUp.begin(), + rawUp.end(), + rawRight.begin(), + decodedInstances.rotations.begin(), + rotationFromUpRight); + + } else if ( + parsedContent.normalUpOct32p.has_value() && + parsedContent.normalRightOct32p.has_value()) { + + gsl::span rawUpOct( + reinterpret_cast( + pBinaryData + *parsedContent.normalUpOct32p), + numInstances); + gsl::span rawRightOct( + reinterpret_cast( + pBinaryData + *parsedContent.normalRightOct32p), + numInstances); + std::transform( + rawUpOct.begin(), + rawUpOct.end(), + rawRightOct.begin(), + decodedInstances.rotations.begin(), + [](auto&& upOct, auto&& rightOct) { + return rotationFromUpRight( + decodeOct32P(upOct), + decodeOct32P(rightOct)); + }); + } else if (decodedInstances.rotationENU) { + glm::dmat4 worldTransform = assetFetcher.tileTransform; + if (decodedInstances.rtcCenter) { + worldTransform = translate(worldTransform, *decodedInstances.rtcCenter); + } + auto worldTransformInv = inverse(worldTransform); + std::transform( + decodedInstances.positions.begin(), + decodedInstances.positions.end(), + decodedInstances.rotations.begin(), + [&](const glm::vec3& position) { + // Find the ENU transform using global coordinates. + glm::dvec4 worldPos = worldTransform * glm::dvec4(position, 1.0); + CesiumGeospatial::LocalHorizontalCoordinateSystem enu( + (glm::dvec3(worldPos))); + const glm::dmat4& ecef = enu.getLocalToEcefTransformation(); + // Express the rotation in the tile's coordinate system, like explicit + // I3dm instance rotations. + glm::dmat4 tileFrame = worldTransformInv * ecef; + return rotationFromUpRight( + glm::vec3(tileFrame[1]), + glm::vec3(tileFrame[0])); + }); + } + decodedInstances.scales.resize(numInstances, glm::vec3(1.0, 1.0, 1.0)); + if (parsedContent.scale.has_value()) { + gsl::span rawScales( + reinterpret_cast(pBinaryData + *parsedContent.scale), + numInstances); + std::transform( + rawScales.begin(), + rawScales.end(), + decodedInstances.scales.begin(), + [](float scaleVal) { return glm::vec3(scaleVal); }); + } + if (parsedContent.scaleNonUniform.has_value()) { + gsl::span rawScalesNonUniform( + reinterpret_cast( + pBinaryData + *parsedContent.scaleNonUniform), + numInstances); + std::transform( + decodedInstances.scales.begin(), + decodedInstances.scales.end(), + rawScalesNonUniform.begin(), + decodedInstances.scales.begin(), + [](auto&& scaleUniform, auto&& scaleNonUniform) { + return scaleUniform * scaleNonUniform; + }); + } + repositionInstances(decodedInstances); + AssetFetcherResult assetFetcherResult; + if (header.gltfFormat == 0) { + // Recursively fetch and read the glTF content. + auto gltfUri = std::string( + reinterpret_cast(gltfData.data()), + gltfData.size()); + return assetFetcher.get(gltfUri) + .thenImmediately( + [options, assetFetcher](AssetFetcherResult&& assetFetcherResult) + -> CesiumAsync::Future { + if (assetFetcherResult.errorList.hasErrors()) { + GltfConverterResult errorResult; + errorResult.errors.merge(assetFetcherResult.errorList); + return assetFetcher.asyncSystem.createResolvedFuture( + std::move(errorResult)); + } + return BinaryToGltfConverter::convert( + assetFetcherResult.bytes, + options, + assetFetcher); + }) + .thenImmediately([convertedI3dm = std::move(convertedI3dm)]( + GltfConverterResult&& converterResult) mutable { + if (converterResult.errors.hasErrors()) { + convertedI3dm.gltfResult.errors.merge(converterResult.errors); + } else { + convertedI3dm.gltfResult = converterResult; + } + return convertedI3dm; + }); + } else { + return BinaryToGltfConverter::convert(gltfData, options, assetFetcher) + .thenImmediately([convertedI3dm = std::move(convertedI3dm)]( + GltfConverterResult&& converterResult) mutable { + if (converterResult.errors.hasErrors()) { + return convertedI3dm; + } + convertedI3dm.gltfResult = converterResult; + return convertedI3dm; + }); + } +} + +glm::dmat4 +composeInstanceTransform(size_t i, const DecodedInstances& decodedInstances) { + glm::dmat4 result(1.0); + if (!decodedInstances.positions.empty()) { + result = translate(result, glm::dvec3(decodedInstances.positions[i])); + } + if (!decodedInstances.rotations.empty()) { + result = result * toMat4(glm::dquat(decodedInstances.rotations[i])); + } + if (!decodedInstances.scales.empty()) { + result = scale(result, glm::dvec3(decodedInstances.scales[i])); + } + return result; +} + +std::vector getMeshGpuInstancingTransforms( + const Model& model, + const ExtensionExtMeshGpuInstancing& gpuExt, + CesiumUtility::ErrorList& errors) { + std::vector instances; + if (gpuExt.attributes.empty()) { + return instances; + } + auto getInstanceAccessor = [&](const char* name) -> const Accessor* { + if (auto accessorItr = gpuExt.attributes.find(name); + accessorItr != gpuExt.attributes.end()) { + return Model::getSafe(&model.accessors, accessorItr->second); + } + return nullptr; + }; + auto errorOut = [&](const std::string& errorMsg) { + errors.emplaceError(errorMsg); + return instances; + }; + + const Accessor* translations = getInstanceAccessor("TRANSLATION"); + const Accessor* rotations = getInstanceAccessor("ROTATION"); + const Accessor* scales = getInstanceAccessor("SCALE"); + int64_t count = 0; + if (translations) { + count = translations->count; + } + if (rotations) { + if (count == 0) { + count = rotations->count; + } else if (count != rotations->count) { + return errorOut(fmt::format( + "instance rotation count {} not consistent with {}", + rotations->count, + count)); + } + } + if (scales) { + if (count == 0) { + count = scales->count; + } else if (count != scales->count) { + return errorOut(fmt::format( + "instance scale count {} not consistent with {}", + scales->count, + count)); + } + } + if (count == 0) { + return errorOut("No valid instance data"); + } + instances.resize(static_cast(count), glm::dmat4(1.0)); + if (translations) { + AccessorView> translationAccessor( + model, + *translations); + if (translationAccessor.status() == AccessorViewStatus::Valid) { + for (unsigned i = 0; i < count; ++i) { + auto transVec = toGlm(translationAccessor[i]); + instances[i] = glm::translate(instances[i], transVec); + } + } else { + return errorOut("invalid accessor for instance translations"); + } + } + if (rotations) { + auto quatAccessorView = getQuaternionAccessorView(model, rotations); + std::visit( + [&](auto&& arg) { + for (unsigned i = 0; i < count; ++i) { + auto quat = toGlmQuat(arg[i]); + instances[i] = instances[i] * glm::toMat4(quat); + } + }, + quatAccessorView); + } + if (scales) { + AccessorView> scaleAccessor( + model, + *scales); + if (scaleAccessor.status() == AccessorViewStatus::Valid) { + for (unsigned i = 0; i < count; ++i) { + auto scaleFactors = toGlm(scaleAccessor[i]); + instances[i] = glm::scale(instances[i], scaleFactors); + } + } else { + return errorOut("invalid accessor for instance translations"); + } + } + return instances; +} + +const size_t rotOffset = sizeof(glm::vec3); +const size_t scaleOffset = rotOffset + sizeof(glm::quat); +const size_t totalStride = + sizeof(glm::vec3) + sizeof(glm::quat) + sizeof(glm::vec3); + +void copyInstanceToBuffer( + const glm::dvec3& position, + const glm::dquat& rotation, + const glm::dvec3& scale, + std::byte* bufferLoc) { + glm::vec3 fposition(position); + std::memcpy(bufferLoc, &fposition, sizeof(fposition)); + glm::quat frotation(rotation); + std::memcpy(bufferLoc + rotOffset, &frotation, sizeof(frotation)); + glm::vec3 fscale(scale); + std::memcpy(bufferLoc + scaleOffset, &fscale, sizeof(fscale)); +} + +void copyInstanceToBuffer( + const glm::dvec3& position, + const glm::dquat& rotation, + const glm::dvec3& scale, + std::vector& bufferData, + size_t i) { + copyInstanceToBuffer(position, rotation, scale, &bufferData[i * totalStride]); +} + +void copyInstanceToBuffer( + const glm::dmat4& instanceTransform, + std::vector& bufferData, + size_t i) { + glm::dvec3 position, scale, skew; + glm::dquat rotation; + glm::dvec4 perspective; + decompose(instanceTransform, scale, rotation, position, skew, perspective); + copyInstanceToBuffer(position, rotation, scale, bufferData, i); +} + +void instantiateGltfInstances( + GltfConverterResult& result, + const DecodedInstances& decodedInstances) { + std::set meshNodes; + int32_t instanceBufferId = createBufferInGltf(*result.model); + auto& instanceBuffer = + result.model->buffers[static_cast(instanceBufferId)]; + int32_t instanceBufferViewId = createBufferViewInGltf( + *result.model, + instanceBufferId, + 0, + static_cast(totalStride)); + auto& instanceBufferView = + result.model->bufferViews[static_cast(instanceBufferViewId)]; + const auto numInstances = + static_cast(decodedInstances.positions.size()); + auto upToZ = CesiumGltfContent::GltfUtilities::applyGltfUpAxisTransform( + *result.model, + glm::dmat4x4(1.0)); + result.model->forEachPrimitiveInScene( + -1, + [&](Model& gltf, + Node& node, + Mesh&, + MeshPrimitive&, + const glm::dmat4& transform) { + auto [nodeItr, inserted] = meshNodes.insert(&node); + if (!inserted) { + return; + } + std::vector modelInstanceTransforms{glm::dmat4(1.0)}; + auto& gpuExt = node.addExtension(); + if (!gpuExt.attributes.empty()) { + // The model already has instances! We will need to create the outer + // product of these instances and those coming from i3dm. + modelInstanceTransforms = getMeshGpuInstancingTransforms( + *result.model, + gpuExt, + result.errors); + if (numInstances * modelInstanceTransforms.size() > + std::numeric_limits::max()) { + result.errors.emplaceError(fmt::format( + "Too many instances: {} from i3dm and {} from glb", + numInstances, + modelInstanceTransforms.size())); + return; + } + } + if (result.errors.hasErrors()) { + return; + } + const uint32_t numNewInstances = static_cast( + numInstances * modelInstanceTransforms.size()); + const size_t instanceDataSize = totalStride * numNewInstances; + auto dataBaseOffset = + static_cast(instanceBuffer.cesium.data.size()); + instanceBuffer.cesium.data.resize(dataBaseOffset + instanceDataSize); + // Transform instance transform into local glTF coordinate system. + const glm::dmat4 toTile = upToZ * transform; + const glm::dmat4 toTileInv = inverse(toTile); + size_t destInstanceIndx = 0; + for (unsigned i = 0; i < numInstances; ++i) { + const glm::dmat4 instanceTransform = + toTileInv * composeInstanceTransform(i, decodedInstances) * + toTile; + for (const auto& modelInstanceTransform : modelInstanceTransforms) { + glm::dmat4 finalTransform = + instanceTransform * modelInstanceTransform; + copyInstanceToBuffer( + finalTransform, + instanceBuffer.cesium.data, + destInstanceIndx++); + } + } + auto posAccessorId = createAccessorInGltf( + gltf, + instanceBufferViewId, + Accessor::ComponentType::FLOAT, + numNewInstances, + Accessor::Type::VEC3); + auto& posAccessor = + gltf.accessors[static_cast(posAccessorId)]; + posAccessor.byteOffset = dataBaseOffset; + gpuExt.attributes["TRANSLATION"] = posAccessorId; + auto rotAccessorId = createAccessorInGltf( + gltf, + instanceBufferViewId, + Accessor::ComponentType::FLOAT, + numInstances, + Accessor::Type::VEC4); + auto& rotAccessor = + gltf.accessors[static_cast(rotAccessorId)]; + rotAccessor.byteOffset = + static_cast(dataBaseOffset + rotOffset); + gpuExt.attributes["ROTATION"] = rotAccessorId; + auto scaleAccessorId = createAccessorInGltf( + gltf, + instanceBufferViewId, + Accessor::ComponentType::FLOAT, + numInstances, + Accessor::Type::VEC3); + auto& scaleAccessor = + gltf.accessors[static_cast(scaleAccessorId)]; + scaleAccessor.byteOffset = + static_cast(dataBaseOffset + scaleOffset); + gpuExt.attributes["SCALE"] = scaleAccessorId; + }); + if (decodedInstances.rtcCenter) { + applyRtcToNodes(*result.model, *decodedInstances.rtcCenter); + } + instanceBuffer.byteLength = + static_cast(instanceBuffer.cesium.data.size()); + instanceBufferView.byteLength = instanceBuffer.byteLength; +} +} // namespace + +CesiumAsync::Future I3dmToGltfConverter::convert( + const gsl::span& instancesBinary, + const CesiumGltfReader::GltfReaderOptions& options, + const AssetFetcher& assetFetcher) { + ConvertedI3dm convertedI3dm; + I3dmHeader header; + uint32_t headerLength = 0; + parseI3dmHeader( + instancesBinary, + header, + headerLength, + convertedI3dm.gltfResult); + if (convertedI3dm.gltfResult.errors) { + return assetFetcher.asyncSystem.createResolvedFuture( + std::move(convertedI3dm.gltfResult)); + } + return convertI3dmContent( + instancesBinary, + header, + headerLength, + options, + assetFetcher, + convertedI3dm) + .thenImmediately([](ConvertedI3dm&& convertedI3dm) { + if (convertedI3dm.gltfResult.errors.hasErrors()) { + return convertedI3dm.gltfResult; + } + instantiateGltfInstances( + convertedI3dm.gltfResult, + convertedI3dm.decodedInstances); + return convertedI3dm.gltfResult; + }); +} +} // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/PntsToGltfConverter.cpp b/Cesium3DTilesContent/src/PntsToGltfConverter.cpp index 660002094..e09996e15 100644 --- a/Cesium3DTilesContent/src/PntsToGltfConverter.cpp +++ b/Cesium3DTilesContent/src/PntsToGltfConverter.cpp @@ -1,5 +1,6 @@ #include "BatchTableToGltfStructuralMetadata.h" +#include #include #include #include @@ -32,13 +33,13 @@ using namespace CesiumUtility; namespace Cesium3DTilesContent { namespace { struct PntsHeader { - unsigned char magic[4]; - uint32_t version; - uint32_t byteLength; - uint32_t featureTableJsonByteLength; - uint32_t featureTableBinaryByteLength; - uint32_t batchTableJsonByteLength; - uint32_t batchTableBinaryByteLength; + unsigned char magic[4] = {0, 0, 0, 0}; + uint32_t version = 0; + uint32_t byteLength = 0; + uint32_t featureTableJsonByteLength = 0; + uint32_t featureTableBinaryByteLength = 0; + uint32_t batchTableJsonByteLength = 0; + uint32_t batchTableBinaryByteLength = 0; }; void parsePntsHeader( @@ -1629,18 +1630,18 @@ void convertPntsContentToGltf( } } // namespace -GltfConverterResult PntsToGltfConverter::convert( +CesiumAsync::Future PntsToGltfConverter::convert( const gsl::span& pntsBinary, - const CesiumGltfReader::GltfReaderOptions& /*options*/) { + const CesiumGltfReader::GltfReaderOptions& /*options*/, + const AssetFetcher& assetFetcher) { GltfConverterResult result; PntsHeader header; uint32_t headerLength = 0; parsePntsHeader(pntsBinary, header, headerLength, result); - if (result.errors) { - return result; + if (!result.errors) { + convertPntsContentToGltf(pntsBinary, header, headerLength, result); } - convertPntsContentToGltf(pntsBinary, header, headerLength, result); - return result; + return assetFetcher.asyncSystem.createResolvedFuture(std::move(result)); } } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/src/registerAllTileContentTypes.cpp b/Cesium3DTilesContent/src/registerAllTileContentTypes.cpp index 11d232f6b..844a31899 100644 --- a/Cesium3DTilesContent/src/registerAllTileContentTypes.cpp +++ b/Cesium3DTilesContent/src/registerAllTileContentTypes.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -11,6 +12,7 @@ void registerAllTileContentTypes() { GltfConverters::registerMagic("glTF", BinaryToGltfConverter::convert); GltfConverters::registerMagic("b3dm", B3dmToGltfConverter::convert); GltfConverters::registerMagic("cmpt", CmptToGltfConverter::convert); + GltfConverters::registerMagic("i3dm", I3dmToGltfConverter::convert); GltfConverters::registerMagic("pnts", PntsToGltfConverter::convert); GltfConverters::registerFileExtension( diff --git a/Cesium3DTilesContent/test/ConvertTileToGltf.cpp b/Cesium3DTilesContent/test/ConvertTileToGltf.cpp new file mode 100644 index 000000000..d74128258 --- /dev/null +++ b/Cesium3DTilesContent/test/ConvertTileToGltf.cpp @@ -0,0 +1,40 @@ +#include "ConvertTileToGltf.h" + +#include +#include +#include + +namespace Cesium3DTilesContent { + +CesiumAsync::AsyncSystem ConvertTileToGltf::asyncSystem( + std::make_shared()); + +AssetFetcher ConvertTileToGltf::makeAssetFetcher(const std::string& baseUrl) { + auto fileAccessor = std::make_shared(); + std::vector requestHeaders; + return AssetFetcher( + asyncSystem, + fileAccessor, + baseUrl, + glm::dmat4(1.0), + requestHeaders); +} + +GltfConverterResult ConvertTileToGltf::fromB3dm( + const std::filesystem::path& filePath, + const CesiumGltfReader::GltfReaderOptions& options) { + AssetFetcher assetFetcher = makeAssetFetcher(""); + auto bytes = readFile(filePath); + auto future = B3dmToGltfConverter::convert(bytes, options, assetFetcher); + return future.wait(); +} + +GltfConverterResult ConvertTileToGltf::fromPnts( + const std::filesystem::path& filePath, + const CesiumGltfReader::GltfReaderOptions& options) { + AssetFetcher assetFetcher = makeAssetFetcher(""); + auto bytes = readFile(filePath); + auto future = PntsToGltfConverter::convert(bytes, options, assetFetcher); + return future.wait(); +} +} // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/test/ConvertTileToGltf.h b/Cesium3DTilesContent/test/ConvertTileToGltf.h index 2d343b59f..12215c308 100644 --- a/Cesium3DTilesContent/test/ConvertTileToGltf.h +++ b/Cesium3DTilesContent/test/ConvertTileToGltf.h @@ -1,7 +1,10 @@ #pragma once #include +#include #include +#include +#include #include #include @@ -10,12 +13,15 @@ namespace Cesium3DTilesContent { class ConvertTileToGltf { public: - static GltfConverterResult fromB3dm(const std::filesystem::path& filePath) { - return B3dmToGltfConverter::convert(readFile(filePath), {}); - } + static GltfConverterResult fromB3dm( + const std::filesystem::path& filePath, + const CesiumGltfReader::GltfReaderOptions& options = {}); + static GltfConverterResult fromPnts( + const std::filesystem::path& filePath, + const CesiumGltfReader::GltfReaderOptions& options = {}); - static GltfConverterResult fromPnts(const std::filesystem::path& filePath) { - return PntsToGltfConverter::convert(readFile(filePath), {}); - } +private: + static CesiumAsync::AsyncSystem asyncSystem; + static AssetFetcher makeAssetFetcher(const std::string& baseUrl); }; } // namespace Cesium3DTilesContent diff --git a/Cesium3DTilesContent/test/TestUpgradeBatchTableToExtStructuralMetadata.cpp b/Cesium3DTilesContent/test/TestUpgradeBatchTableToExtStructuralMetadata.cpp index b419df9d9..87781484f 100644 --- a/Cesium3DTilesContent/test/TestUpgradeBatchTableToExtStructuralMetadata.cpp +++ b/Cesium3DTilesContent/test/TestUpgradeBatchTableToExtStructuralMetadata.cpp @@ -1065,7 +1065,7 @@ TEST_CASE("Draco-compressed b3dm uses _FEATURE_ID_0 attribute name in glTF") { options.decodeDraco = false; GltfConverterResult result = - B3dmToGltfConverter::convert(readFile(testFilePath), options); + ConvertTileToGltf::fromB3dm(testFilePath, options); CHECK(result.errors.errors.empty()); CHECK(result.errors.warnings.empty()); REQUIRE(result.model); diff --git a/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp b/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp index d6f9e1c94..a67d41aad 100644 --- a/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp +++ b/Cesium3DTilesSelection/src/ImplicitOctreeLoader.cpp @@ -104,23 +104,31 @@ CesiumAsync::Future requestTileContent( const std::string& tileUrl, const std::vector& requestHeaders, CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets, - bool applyTextureTransform) { + bool applyTextureTransform, + const glm::dmat4& tileTransform) { return pAssetAccessor->get(asyncSystem, tileUrl, requestHeaders) .thenInWorkerThread([pLogger, ktx2TranscodeTargets, - applyTextureTransform]( + applyTextureTransform, + &asyncSystem, + pAssetAccessor, + tileTransform, + requestHeaders]( std::shared_ptr&& pCompletedRequest) mutable { const CesiumAsync::IAssetResponse* pResponse = pCompletedRequest->response(); + auto fail = [&]() { + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult(std::move(pCompletedRequest))); + }; const std::string& tileUrl = pCompletedRequest->url(); if (!pResponse) { SPDLOG_LOGGER_ERROR( pLogger, "Did not receive a valid response for tile content {}", tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); + return fail(); } uint16_t statusCode = pResponse->statusCode(); @@ -130,8 +138,7 @@ CesiumAsync::Future requestTileContent( "Received status code {} for tile content {}", statusCode, tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); + return fail(); } // find gltf converter @@ -147,28 +154,35 @@ CesiumAsync::Future requestTileContent( CesiumGltfReader::GltfReaderOptions gltfOptions; gltfOptions.ktx2TranscodeTargets = ktx2TranscodeTargets; gltfOptions.applyTextureTransform = applyTextureTransform; - GltfConverterResult result = converter(responseData, gltfOptions); - - // Report any errors if there are any - logTileLoadResult(pLogger, tileUrl, result.errors); - if (result.errors || !result.model) { - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - return TileLoadResult{ - std::move(*result.model), - CesiumGeometry::Axis::Y, - std::nullopt, - std::nullopt, - std::nullopt, - std::move(pCompletedRequest), - {}, - TileLoadResultState::Success}; + AssetFetcher assetFetcher{ + asyncSystem, + pAssetAccessor, + tileUrl, + tileTransform, + requestHeaders}; + return converter(responseData, gltfOptions, assetFetcher) + .thenImmediately([pLogger, tileUrl, pCompletedRequest]( + GltfConverterResult&& result) { + // Report any errors if there are any + logTileLoadResult(pLogger, tileUrl, result.errors); + if (result.errors || !result.model) { + return TileLoadResult::createFailedResult( + std::move(pCompletedRequest)); + } + + return TileLoadResult{ + std::move(*result.model), + CesiumGeometry::Axis::Y, + std::nullopt, + std::nullopt, + std::nullopt, + std::move(pCompletedRequest), + {}, + TileLoadResultState::Success}; + }); } - // content type is not supported - return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); + return fail(); }); } } // namespace @@ -261,7 +275,8 @@ ImplicitOctreeLoader::loadTileContent(const TileLoadInput& loadInput) { tileUrl, requestHeaders, contentOptions.ktx2TranscodeTargets, - contentOptions.applyTextureTransform); + contentOptions.applyTextureTransform, + tile.getTransform()); } TileChildrenResult ImplicitOctreeLoader::createTileChildren(const Tile& tile) { diff --git a/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp b/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp index 0432aa53d..d2833425a 100644 --- a/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp +++ b/Cesium3DTilesSelection/src/ImplicitQuadtreeLoader.cpp @@ -114,23 +114,31 @@ CesiumAsync::Future requestTileContent( const std::string& tileUrl, const std::vector& requestHeaders, CesiumGltf::Ktx2TranscodeTargets ktx2TranscodeTargets, - bool applyTextureTransform) { + bool applyTextureTransform, + const glm::dmat4& tileTransform) { return pAssetAccessor->get(asyncSystem, tileUrl, requestHeaders) .thenInWorkerThread([pLogger, ktx2TranscodeTargets, - applyTextureTransform]( + applyTextureTransform, + &asyncSystem, + pAssetAccessor, + tileTransform, + requestHeaders]( std::shared_ptr&& pCompletedRequest) mutable { const CesiumAsync::IAssetResponse* pResponse = pCompletedRequest->response(); + auto fail = [&]() { + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult(std::move(pCompletedRequest))); + }; const std::string& tileUrl = pCompletedRequest->url(); if (!pResponse) { SPDLOG_LOGGER_ERROR( pLogger, "Did not receive a valid response for tile content {}", tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); + return fail(); } uint16_t statusCode = pResponse->statusCode(); @@ -140,8 +148,7 @@ CesiumAsync::Future requestTileContent( "Received status code {} for tile content {}", statusCode, tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); + return fail(); } // find gltf converter @@ -157,28 +164,35 @@ CesiumAsync::Future requestTileContent( CesiumGltfReader::GltfReaderOptions gltfOptions; gltfOptions.ktx2TranscodeTargets = ktx2TranscodeTargets; gltfOptions.applyTextureTransform = applyTextureTransform; - GltfConverterResult result = converter(responseData, gltfOptions); - - // Report any errors if there are any - logTileLoadResult(pLogger, tileUrl, result.errors); - if (result.errors || !result.model) { - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - return TileLoadResult{ - std::move(*result.model), - CesiumGeometry::Axis::Y, - std::nullopt, - std::nullopt, - std::nullopt, - std::move(pCompletedRequest), - {}, - TileLoadResultState::Success}; + AssetFetcher assetFetcher{ + asyncSystem, + pAssetAccessor, + tileUrl, + tileTransform, + requestHeaders}; + return converter(responseData, gltfOptions, assetFetcher) + .thenImmediately([pLogger, tileUrl, pCompletedRequest]( + GltfConverterResult&& result) { + // Report any errors if there are any + logTileLoadResult(pLogger, tileUrl, result.errors); + if (result.errors || !result.model) { + return TileLoadResult::createFailedResult( + std::move(pCompletedRequest)); + } + + return TileLoadResult{ + std::move(*result.model), + CesiumGeometry::Axis::Y, + std::nullopt, + std::nullopt, + std::nullopt, + std::move(pCompletedRequest), + {}, + TileLoadResultState::Success}; + }); } - // content type is not supported - return TileLoadResult::createFailedResult(std::move(pCompletedRequest)); + return fail(); }); } } // namespace @@ -300,7 +314,8 @@ ImplicitQuadtreeLoader::loadTileContent(const TileLoadInput& loadInput) { tileUrl, requestHeaders, contentOptions.ktx2TranscodeTargets, - contentOptions.applyTextureTransform); + contentOptions.applyTextureTransform, + tile.getTransform()); } TileChildrenResult diff --git a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp index 70e0a028b..f9b021e8f 100644 --- a/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp +++ b/Cesium3DTilesSelection/src/TilesetJsonLoader.cpp @@ -860,80 +860,90 @@ TilesetJsonLoader::loadTileContent(const TileLoadInput& loadInput) { std::string resolvedUrl = CesiumUtility::Uri::resolve(this->_baseUrl, *url, true); return pAssetAccessor->get(asyncSystem, resolvedUrl, requestHeaders) - .thenInWorkerThread( - [pLogger, - contentOptions, - tileTransform, - tileRefine, - upAxis = _upAxis, - externalContentInitializer = std::move(externalContentInitializer)]( - std::shared_ptr&& - pCompletedRequest) mutable { - auto pResponse = pCompletedRequest->response(); - const std::string& tileUrl = pCompletedRequest->url(); - if (!pResponse) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Did not receive a valid response for tile content {}", - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - uint16_t statusCode = pResponse->statusCode(); - if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { - SPDLOG_LOGGER_ERROR( - pLogger, - "Received status code {} for tile content {}", - statusCode, - tileUrl); - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - // find gltf converter - const auto& responseData = pResponse->data(); - auto converter = GltfConverters::getConverterByMagic(responseData); - if (!converter) { - converter = GltfConverters::getConverterByFileExtension(tileUrl); - } - - if (converter) { - // Convert to gltf - CesiumGltfReader::GltfReaderOptions gltfOptions; - gltfOptions.ktx2TranscodeTargets = - contentOptions.ktx2TranscodeTargets; - gltfOptions.applyTextureTransform = - contentOptions.applyTextureTransform; - GltfConverterResult result = converter(responseData, gltfOptions); - - // Report any errors if there are any - logTileLoadResult(pLogger, tileUrl, result.errors); - if (result.errors) { - return TileLoadResult::createFailedResult( - std::move(pCompletedRequest)); - } - - return TileLoadResult{ - std::move(*result.model), - upAxis, - std::nullopt, - std::nullopt, - std::nullopt, - std::move(pCompletedRequest), - {}, - TileLoadResultState::Success}; - } else { - // not a renderable content, then it must be external tileset - return parseExternalTilesetInWorkerThread( + .thenInWorkerThread([pLogger, + contentOptions, + tileTransform, + tileRefine, + upAxis = _upAxis, + externalContentInitializer = + std::move(externalContentInitializer), + pAssetAccessor, + &asyncSystem, + requestHeaders]( + std::shared_ptr&& + pCompletedRequest) mutable { + auto pResponse = pCompletedRequest->response(); + const std::string& tileUrl = pCompletedRequest->url(); + if (!pResponse) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Did not receive a valid response for tile content {}", + tileUrl); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult(std::move(pCompletedRequest))); + } + + uint16_t statusCode = pResponse->statusCode(); + if (statusCode != 0 && (statusCode < 200 || statusCode >= 300)) { + SPDLOG_LOGGER_ERROR( + pLogger, + "Received status code {} for tile content {}", + statusCode, + tileUrl); + return asyncSystem.createResolvedFuture( + TileLoadResult::createFailedResult(std::move(pCompletedRequest))); + } + + // find gltf converter + const auto& responseData = pResponse->data(); + auto converter = GltfConverters::getConverterByMagic(responseData); + if (!converter) { + converter = GltfConverters::getConverterByFileExtension(tileUrl); + } + + if (converter) { + // Convert to gltf + AssetFetcher assetFetcher{ + asyncSystem, + pAssetAccessor, + tileUrl, + tileTransform, + requestHeaders}; + CesiumGltfReader::GltfReaderOptions gltfOptions; + gltfOptions.ktx2TranscodeTargets = + contentOptions.ktx2TranscodeTargets; + gltfOptions.applyTextureTransform = + contentOptions.applyTextureTransform; + return converter(responseData, gltfOptions, assetFetcher) + .thenImmediately([pLogger, upAxis, tileUrl, pCompletedRequest]( + GltfConverterResult&& result) { + logTileLoadResult(pLogger, tileUrl, result.errors); + if (result.errors) { + return TileLoadResult::createFailedResult( + std::move(pCompletedRequest)); + } + return TileLoadResult{ + std::move(*result.model), + upAxis, + std::nullopt, + std::nullopt, + std::nullopt, + std::move(pCompletedRequest), + {}, + TileLoadResultState::Success}; + }); + } else { + // not a renderable content, then it must be external tileset + return asyncSystem.createResolvedFuture( + parseExternalTilesetInWorkerThread( tileTransform, upAxis, tileRefine, pLogger, std::move(pCompletedRequest), - std::move(externalContentInitializer)); - } - }); + std::move(externalContentInitializer))); + } + }); } TileChildrenResult TilesetJsonLoader::createTileChildren(const Tile& tile) { diff --git a/CesiumGltf/include/CesiumGltf/AccessorUtility.h b/CesiumGltf/include/CesiumGltf/AccessorUtility.h index 527172a21..438bd01a3 100644 --- a/CesiumGltf/include/CesiumGltf/AccessorUtility.h +++ b/CesiumGltf/include/CesiumGltf/AccessorUtility.h @@ -308,4 +308,21 @@ struct TexCoordFromAccessor { int64_t index; }; +/** + * Type definition for quaternion accessors, as used in ExtMeshGpuInstancing + * rotations and animation samplers. + */ +typedef std::variant< + AccessorView>, + AccessorView>, + AccessorView>, + AccessorView>, + AccessorView>> + QuaternionAccessorType; + +QuaternionAccessorType +getQuaternionAccessorView(const Model& model, const Accessor* accessor); + +QuaternionAccessorType +getQuaternionAccessorView(const Model& model, int32_t accessorIndex); } // namespace CesiumGltf diff --git a/CesiumGltf/src/AccessorUtility.cpp b/CesiumGltf/src/AccessorUtility.cpp index 5a4383b21..35ae406c1 100644 --- a/CesiumGltf/src/AccessorUtility.cpp +++ b/CesiumGltf/src/AccessorUtility.cpp @@ -133,4 +133,38 @@ TexCoordAccessorType getTexCoordAccessorView( } } +QuaternionAccessorType +getQuaternionAccessorView(const Model& model, const Accessor* pAccessor) { + if (!pAccessor) { + return QuaternionAccessorType(); + } + switch (pAccessor->componentType) { + case Accessor::ComponentType::BYTE: + return AccessorView>(model, *pAccessor); + [[fallthrough]]; + case Accessor::ComponentType::UNSIGNED_BYTE: + return AccessorView>(model, *pAccessor); + [[fallthrough]]; + case Accessor::ComponentType::SHORT: + return AccessorView>(model, *pAccessor); + [[fallthrough]]; + case Accessor::ComponentType::UNSIGNED_SHORT: + return AccessorView>(model, *pAccessor); + [[fallthrough]]; + case Accessor::ComponentType::FLOAT: + return AccessorView>(model, *pAccessor); + default: + return QuaternionAccessorType(); + } +} + +QuaternionAccessorType +getQuaternionAccessorView(const Model& model, int32_t accessorIndex) { + const Accessor* pAccessor = + model.getSafe(&model.accessors, accessorIndex); + if (!pAccessor || pAccessor->type != Accessor::Type::VEC4) { + return QuaternionAccessorType(); + } + return getQuaternionAccessorView(model, pAccessor); +} } // namespace CesiumGltf diff --git a/CesiumNativeTests/include/CesiumNativeTests/FileAccessor.h b/CesiumNativeTests/include/CesiumNativeTests/FileAccessor.h new file mode 100644 index 000000000..9a68a3e18 --- /dev/null +++ b/CesiumNativeTests/include/CesiumNativeTests/FileAccessor.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include + +namespace CesiumNativeTests { +class FileAccessor : public CesiumAsync::IAssetAccessor { +public: + CesiumAsync::Future> + get(const CesiumAsync::AsyncSystem& asyncSystem, + const std::string& url, + const std::vector&) override; + + CesiumAsync::Future> request( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::string& verb, + const std::string& url, + const std::vector& headers, + const gsl::span&) override; + + void tick() noexcept override {} +}; +} // namespace CesiumNativeTests diff --git a/CesiumNativeTests/include/CesiumNativeTests/RandomVector.h b/CesiumNativeTests/include/CesiumNativeTests/RandomVector.h new file mode 100644 index 000000000..7cfc630ba --- /dev/null +++ b/CesiumNativeTests/include/CesiumNativeTests/RandomVector.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace CesiumNativeTests { + +// Produce a random N-dimensional unit vector. Use a constant seed in order to +// get a repeatable stream of vectors that can then be debugged! +template struct RandomUnitVectorGenerator { + using value_type = typename Vec::value_type; + std::mt19937 gen; + std::uniform_real_distribution dis; + + RandomUnitVectorGenerator() + : dis(std::nextafter(static_cast(-1), value_type()), + static_cast(1)) { + gen.seed(42); + } + + Vec operator()() { + Vec result(0); + value_type length2 = 0; + // If we don't discard values that fall outside the unit sphere, then the + // points are biased towards the corners of the unit cube. + do { + for (glm::length_t i = 0; i < Vec::length(); ++i) { + result[i] = dis(gen); + } + length2 = dot(result, result); + } while (length2 > 1 || length2 == 0); + return result / std::sqrt(length2); + } +}; + +template struct RandomQuaternionGenerator { + RandomUnitVectorGenerator> vecGenerator; + + glm::qua operator()() { + glm::vec<4, T> vec = vecGenerator(); + return glm::qua(vec.w, vec.x, vec.y, vec.z); + } +}; +} // namespace CesiumNativeTests diff --git a/CesiumNativeTests/src/FileAccessor.cpp b/CesiumNativeTests/src/FileAccessor.cpp new file mode 100644 index 000000000..cc94e8716 --- /dev/null +++ b/CesiumNativeTests/src/FileAccessor.cpp @@ -0,0 +1,79 @@ +#include +#include + +#include +#include + +namespace CesiumNativeTests { + +namespace { +std::unique_ptr readFileUri(const std::string& uri) { + + std::vector result; + CesiumAsync::HttpHeaders headers; + std::string contentType; + auto response = [&](uint16_t errorCode) { + return std::make_unique( + errorCode, + contentType, + headers, + std::move(result)); + }; + auto protocolPos = uri.find("file:///"); + if (protocolPos != 0) { + return response(400); + } + std::string path = uri.substr(std::strlen("file://")); + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) { + return response(404); + } + std::streamsize size = file.tellg(); + file.seekg(0, std::ios::beg); + result.resize(static_cast(size)); + file.read(reinterpret_cast(result.data()), size); + if (!file) { + return response(503); + } else { + contentType = "application/octet-stream"; + headers.insert({"content-type", contentType}); + return response(200); + } +} +} // namespace + +CesiumAsync::Future> +FileAccessor::get( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::string& url, + const std::vector& headers) { + return asyncSystem.createFuture>( + [&](const auto& promise) { + auto response = readFileUri(url); + CesiumAsync::HttpHeaders cesiumHeaders(headers.begin(), headers.end()); + auto request = std::make_shared( + "GET", + url, + cesiumHeaders, + std::move(response)); + promise.resolve(request); + }); +} + +// Can we do anything with a request that isn't a GET? +CesiumAsync::Future> +FileAccessor::request( + const CesiumAsync::AsyncSystem& asyncSystem, + const std::string& verb, + const std::string& url, + const std::vector& headers, + const gsl::span&) { + if (verb == "GET") { + return get(asyncSystem, url, headers); + } + return asyncSystem.createFuture>( + [&](const auto& promise) { + promise.reject(std::runtime_error("unsupported operation")); + }); +} +} // namespace CesiumNativeTests diff --git a/CesiumUtility/include/CesiumUtility/Math.h b/CesiumUtility/include/CesiumUtility/Math.h index b7a021fb9..ded8212d9 100644 --- a/CesiumUtility/include/CesiumUtility/Math.h +++ b/CesiumUtility/include/CesiumUtility/Math.h @@ -451,6 +451,53 @@ class CESIUMUTILITY_API Math final { return down; } } + + /** + * @brief Construct a vector perpendicular to the argument. + * @param v The input vector + * @return A vector perpendicular to the input vector + */ + template + static glm::vec<3, T, Q> perpVec(const glm::vec<3, T, Q>& v) { + // This constructs a vector whose dot product with v will be 0, hence + // perpendicular to v. As seen in the "Physically Based Rendering". + if (std::abs(v.x) > std::abs(v.y)) { + return glm::vec<3, T, Q>(-v.z, 0, v.x) / std::sqrt(v.x * v.x + v.z * v.z); + } + return glm::vec<3, T, Q>(0, v.z, -v.y) / std::sqrt(v.y * v.y + v.z * v.z); + } + + /** @brief Compute the rotation between two unit vectors. + * @param vec1 The first vector. + * @param vec2 The second vector. + * @return A quaternion representing the rotation of vec1 to vec2. + */ + template + static glm::qua + rotation(const glm::vec<3, T, Q>& vec1, const glm::vec<3, T, Q>& vec2) { + // If we take the dot and cross products of the two vectors and store + // them in a quaternion, that quaternion represents twice the required + // rotation. We get the correct quaternion by "averaging" with the zero + // rotation quaternion, in a way analagous to finding the half vector + // between two 3D vectors. + auto cosRot = dot(vec1, vec2); + auto rotAxis = cross(vec1, vec2); + auto rotAxisLen2 = dot(rotAxis, rotAxis); + // Not using epsilon for these tests. If abs(cosRot) < 1.0, we can still + // create a sensible rotation. + if (cosRot >= 1 || (rotAxisLen2 == 0 && cosRot > 0)) { + // zero rotation + return glm::qua(1, 0, 0, 0); + } + if (cosRot <= -1 || (rotAxisLen2 == 0 && cosRot < 0)) { + auto perpAxis = CesiumUtility::Math::perpVec(vec1); + // rotation by pi radians + return glm::qua(0, perpAxis); + } + + glm::qua sumQuat(cosRot + 1, rotAxis); + return normalize(sumQuat); + } }; } // namespace CesiumUtility diff --git a/CesiumUtility/test/TestMath.cpp b/CesiumUtility/test/TestMath.cpp index 1eb21aef2..e84db887b 100644 --- a/CesiumUtility/test/TestMath.cpp +++ b/CesiumUtility/test/TestMath.cpp @@ -1,6 +1,8 @@ +#include "CesiumNativeTests/RandomVector.h" #include "CesiumUtility/Math.h" #include +#include using namespace CesiumUtility; @@ -144,3 +146,52 @@ TEST_CASE("Math::mod") { CHECK(Math::mod(-1.0, -1.0) == -0.0); CHECK(Math::equalsEpsilon(Math::mod(-1.1, -1.0), -0.1, Math::Epsilon15)); } + +TEST_CASE("Math::perpVec") { + glm::vec3 v0(.2f, .3f, .4f); + glm::vec3 perp = Math::perpVec(v0); + // perp is normalized + glm::vec3 mutual = glm::cross(v0, perp); + CHECK(Math::equalsEpsilon(length(v0), length(mutual), Math::Epsilon5)); + glm::vec3 v1(.3f, .2f, -1.0f); + glm::vec3 perp1 = Math::perpVec(v1); + glm::vec3 mutual1 = glm::cross(v1, perp1); + CHECK(Math::equalsEpsilon(length(v1), length(mutual1), Math::Epsilon5)); +} + +TEST_CASE("Math::rotation") { + CesiumNativeTests::RandomUnitVectorGenerator generator; + for (int i = 0; i < 100; ++i) { + glm::vec3 vec1 = generator(); + glm::vec3 vec2 = generator(); + glm::quat rotation = Math::rotation(vec1, vec2); + // Not a unit vector! + glm::vec3 axis(rotation.x, rotation.y, rotation.z); + // Is the rotation axis perpendicular to vec1 and vec2? + CHECK(Math::equalsEpsilon(dot(vec1, axis), 0.0f, Math::Epsilon5)); + CHECK(Math::equalsEpsilon(dot(vec2, axis), 0.0f, Math::Epsilon5)); + // Does the quaternion match the trig values we get with dot and cross? + const float c = dot(vec1, vec2); + const float s = length(cross(vec1, vec2)); + const float qc = rotation.w; + const float qs = length(axis); + // Double angle formulae + float testSin = 2.0f * qs * qc; + float testCos = qc * qc - qs * qs; + CHECK(Math::equalsEpsilon(s, testSin, Math::Epsilon5)); + CHECK(Math::equalsEpsilon(c, testCos, Math::Epsilon5)); + } + for (int i = 0; i < 100; ++i) { + glm::vec3 vec = generator(); + glm::quat rotation = Math::rotation(vec, vec); + CHECK(Math::equalsEpsilon(rotation.w, 1.0f, Math::Epsilon5)); + } + for (int i = 0; i < 100; ++i) { + glm::vec3 vec1 = generator(); + glm::vec3 vec2 = vec1 * -1.0f; + glm::quat rotation = Math::rotation(vec1, vec2); + glm::vec3 axis(rotation.x, rotation.y, rotation.z); + CHECK(Math::equalsEpsilon(rotation.w, 0.0f, Math::Epsilon5)); + CHECK(Math::equalsEpsilon(dot(vec1, axis), 0.0f, Math::Epsilon5)); + } +} diff --git a/package-lock.json b/package-lock.json index ed16c2a54..d0980523a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cesium-native", - "version": "0.33.0", + "version": "0.35.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cesium-native", - "version": "0.33.0", + "version": "0.35.0", "license": "Apache-2.0", "devDependencies": { "clang-format": "^1.5.0"