From eefb1138533e0476c9bfb8a2557923359066667f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 27 Sep 2024 07:22:35 +1000 Subject: [PATCH 01/23] WIP texture loading reorg. Mostly just broken so far. --- .../Private/CesiumGltfTextures.cpp | 267 ++++++ .../Private/CesiumGltfTextures.h | 24 + .../Private/CesiumTextureUtility.cpp | 767 +++++++++--------- .../Private/CesiumTextureUtility.h | 18 +- extern/cesium-native | 2 +- 5 files changed, 702 insertions(+), 376 deletions(-) create mode 100644 Source/CesiumRuntime/Private/CesiumGltfTextures.cpp create mode 100644 Source/CesiumRuntime/Private/CesiumGltfTextures.h diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp new file mode 100644 index 000000000..a6fff0bbb --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -0,0 +1,267 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#include "CesiumGltfTextures.h" +#include "CesiumRuntime.h" +#include "CesiumTextureResource.h" +#include "CesiumTextureUtility.h" +#include +#include +#include +#include + +using namespace CesiumAsync; +using namespace CesiumGltf; + +namespace { + +// Determines if a glTF primitive is usable for our purposes. +bool isValidPrimitive(Model& gltf, MeshPrimitive& primitive); + +// Determines if an Accessor's componentType is valid for an index buffer. +bool isSupportedIndexComponentType(int32_t componentType); + +// Determines if the given Primitive mode is one that we support. +bool isSupportedPrimitiveMode(int32_t primitiveMode); + +// Determines if the given texture uses mipmaps. +bool doesTextureUseMipmaps(const Model& gltf, const Texture& texture); + +// Creates a single texture in the load thread. +SharedFuture createTextureInLoadThread( + const AsyncSystem& asyncSystem, + Model& gltf, + TextureInfo& textureInfo, + bool sRGB, + const std::vector& imageNeedsMipmaps); + +} // namespace + +/*static*/ CesiumAsync::Future CesiumGltfTextures::createInLoadThread( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::Model& model) { + // This array is parallel to model.images and indicates whether each image + // requires mipmaps. An image requires mipmaps if any of its textures have a + // sampler that will use them. + std::vector imageNeedsMipmaps(model.images.size(), false); + for (const Texture& texture : model.textures) { + int32_t imageIndex = texture.source; + if (imageIndex < 0 || imageIndex >= model.images.size()) { + continue; + } + + if (!imageNeedsMipmaps[imageIndex]) { + imageNeedsMipmaps[imageIndex] = doesTextureUseMipmaps(model, texture); + } + } + + std::vector> futures; + + model.forEachPrimitiveInScene( + -1, + [&imageNeedsMipmaps, &asyncSystem, &futures]( + Model& gltf, + Node& node, + Mesh& mesh, + MeshPrimitive& primitive, + const glm::dmat4& transform) { + if (!isValidPrimitive(gltf, primitive)) { + return; + } + + Material* pMaterial = + Model::getSafe(&gltf.materials, primitive.material); + if (!pMaterial) { + // A primitive using the default material will not have any textures. + return; + } + + if (pMaterial->pbrMetallicRoughness) { + if (pMaterial->pbrMetallicRoughness->baseColorTexture) { + futures.emplace_back(createTextureInLoadThread( + asyncSystem, + gltf, + *pMaterial->pbrMetallicRoughness->baseColorTexture, + true, + imageNeedsMipmaps)); + } + if (pMaterial->pbrMetallicRoughness->metallicRoughnessTexture) { + futures.emplace_back(createTextureInLoadThread( + asyncSystem, + gltf, + *pMaterial->pbrMetallicRoughness->metallicRoughnessTexture, + false, + imageNeedsMipmaps)); + } + } + + if (pMaterial->emissiveTexture) + futures.emplace_back(createTextureInLoadThread( + asyncSystem, + gltf, + *pMaterial->emissiveTexture, + true, + imageNeedsMipmaps)); + if (pMaterial->normalTexture) + futures.emplace_back(createTextureInLoadThread( + asyncSystem, + gltf, + *pMaterial->normalTexture, + false, + imageNeedsMipmaps)); + if (pMaterial->occlusionTexture) + futures.emplace_back(createTextureInLoadThread( + asyncSystem, + gltf, + *pMaterial->occlusionTexture, + false, + imageNeedsMipmaps)); + }); + + return asyncSystem.all(std::move(futures)); +} + +namespace { + +bool isSupportedIndexComponentType(int32_t componentType) { + return componentType == Accessor::ComponentType::UNSIGNED_BYTE || + componentType == Accessor::ComponentType::UNSIGNED_SHORT || + componentType == Accessor::ComponentType::UNSIGNED_INT; +} + +bool isSupportedPrimitiveMode(int32_t primitiveMode) { + return primitiveMode == MeshPrimitive::Mode::TRIANGLES || + primitiveMode == MeshPrimitive::Mode::TRIANGLE_STRIP || + primitiveMode == MeshPrimitive::Mode::POINTS; +} + +// Determines if a glTF primitive is usable for our purposes. +bool isValidPrimitive( + CesiumGltf::Model& gltf, + CesiumGltf::MeshPrimitive& primitive) { + if (!isSupportedPrimitiveMode(primitive.mode)) { + // This primitive's mode is not supported. + return false; + } + + auto positionAccessorIt = + primitive.attributes.find(AttributeSemantics::POSITION); + if (positionAccessorIt == primitive.attributes.end()) { + // This primitive doesn't have a POSITION semantic, so it's not valid. + return false; + } + + AccessorView positionView(gltf, positionAccessorIt->second); + if (positionView.status() != AccessorViewStatus::Valid) { + // This primitive's POSITION accessor is invalid, so the primitive is not + // valid. + return false; + } + + Accessor* pIndexAccessor = Model::getSafe(&gltf.accessors, primitive.indices); + if (pIndexAccessor && + !isSupportedIndexComponentType(pIndexAccessor->componentType)) { + // This primitive's indices are not a supported type, so the primitive is + // not valid. + return false; + } + + return true; +} + +bool doesTextureUseMipmaps(const Model& gltf, const Texture& texture) { + const Sampler& sampler = Model::getSafe(gltf.samplers, texture.sampler); + + switch (sampler.minFilter.value_or( + CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return true; + default: // LINEAR and NEAREST + return false; + } +} + +struct ExtensionUnrealTextureResource { + static inline constexpr const char* TypeName = + "ExtensionUnrealTextureResource"; + static inline constexpr const char* ExtensionName = + "PRIVATE_unreal_texture_resource"; + + ExtensionUnrealTextureResource() {} + + TSharedPtr pTextureResource = nullptr; + std::optional> createFuture = std::nullopt; +}; + +std::mutex textureResourceMutex; + +// Returns a Future that will resolve when the image is loaded. It _may_ also +// return a Promise, in which case the calling thread is responsible for doing +// the loading and should resolve the Promise when it's done. +std::pair, std::optional>> +getOrCreateImageFuture(const AsyncSystem& asyncSystem, Image& image) { + std::scoped_lock lock(textureResourceMutex); + + ExtensionUnrealTextureResource& extension = + image.cesium->addExtension(); + if (extension.createFuture) { + // Another thread is already working on this image. + return {*extension.createFuture, std::nullopt}; + } else { + // This thread will work on this image. + Promise promise = asyncSystem.createPromise(); + return {promise.getFuture().share(), promise}; + } +} + +SharedFuture createTextureInLoadThread( + const AsyncSystem& asyncSystem, + Model& gltf, + TextureInfo& textureInfo, + bool sRGB, + const std::vector& imageNeedsMipmaps) { + Texture* pTexture = Model::getSafe(&gltf.textures, textureInfo.index); + if (pTexture == nullptr) + return asyncSystem.createResolvedFuture().share(); + + Image* pImage = Model::getSafe(&gltf.images, pTexture->source); + if (pImage == nullptr) + return asyncSystem.createResolvedFuture().share(); + + check(pTexture->source >= 0 && pTexture->source < imageNeedsMipmaps.size()); + bool needsMips = imageNeedsMipmaps[pTexture->source]; + + auto [future, maybePromise] = getOrCreateImageFuture(asyncSystem, *pImage); + if (!maybePromise) { + // Another thread is already loading this image. + return future; + } + + // Proceed to load the image in this thread. + ImageCesium& cesium = *pImage->cesium; + + if (needsMips && !cesium.pixelData.empty()) { + std::optional errorMessage = + CesiumGltfReader::GltfReader::generateMipMaps(cesium); + if (errorMessage) { + UE_LOG( + LogCesium, + Warning, + TEXT("%s"), + UTF8_TO_TCHAR(errorMessage->c_str())); + } + } + + CesiumTextureUtility::loadTextureFromModelAnyThreadPart( + gltf, + *pTexture, + sRGB); + + maybePromise->resolve(); + + return future; +} + +} // namespace diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.h b/Source/CesiumRuntime/Private/CesiumGltfTextures.h new file mode 100644 index 000000000..339481b1b --- /dev/null +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.h @@ -0,0 +1,24 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include + +namespace CesiumAsync { +class AsyncSystem; +} + +namespace CesiumGltf { +struct Model; +} + +class CesiumGltfTextures { +public: + /** + * Creates all of the textures that are required by the given glTF, and adds + * `ExtensionUnrealTexture` to each. + */ + static CesiumAsync::Future createInLoadThread( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::Model& model); +}; diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index b7aa9e52f..19c3ea3f6 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -265,18 +265,18 @@ void ReferenceCountedUnrealTexture::setUnrealTexture( this->_pUnrealTexture = p; } -const TUniquePtr& +const TSharedPtr& ReferenceCountedUnrealTexture::getTextureResource() const { return this->_pTextureResource; } -TUniquePtr& +TSharedPtr& ReferenceCountedUnrealTexture::getTextureResource() { return this->_pTextureResource; } void ReferenceCountedUnrealTexture::setTextureResource( - TUniquePtr&& p) { + TSharedPtr&& p) { this->_pTextureResource = std::move(p); } @@ -290,7 +290,7 @@ void ReferenceCountedUnrealTexture::setSharedImage( this->_pImageCesium = image; } -TUniquePtr createTextureResourceFromImageCesium( +TSharedPtr createTextureResourceFromImageCesium( CesiumGltf::ImageCesium& imageCesium, TextureAddress addressX, TextureAddress addressY, @@ -307,8 +307,8 @@ TUniquePtr createTextureResourceFromImageCesium( FTexture2DRHIRef textureReference = CreateRHITexture2D_Async(imageCesium, pixelFormat, sRGB); - TUniquePtr textureResource = - MakeUnique( + TSharedPtr textureResource = + MakeShared( textureReference, group, imageCesium.width, @@ -339,7 +339,7 @@ TUniquePtr createTextureResourceFromImageCesium( return nullptr; } - return MakeUnique( + return MakeShared( std::move(imageCesium), group, imageCesium.width, @@ -372,7 +372,7 @@ struct ExtensionUnrealTextureResource { std::optional> preprocessFuture = std::nullopt; - static TUniquePtr loadTextureResource( + static TSharedPtr loadTextureResource( CesiumGltf::ImageCesium& imageCesium, TextureAddress addressX, TextureAddress addressY, @@ -396,7 +396,7 @@ struct ExtensionUnrealTextureResource { // Already have a texture resource, just use that. if (extension.pTextureResource != nullptr) { - return MakeUnique( + return MakeShared( extension.pTextureResource.Get(), group, imageCesium.width, @@ -416,7 +416,7 @@ struct ExtensionUnrealTextureResource { // caching purposes. imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); - TUniquePtr textureResource = + TSharedPtr textureResource = createTextureResourceFromImageCesium( imageCesium, addressX, @@ -429,423 +429,448 @@ struct ExtensionUnrealTextureResource { check(textureResource != nullptr); - extension.pTextureResource = - TSharedPtr(textureResource.Release()); - return MakeUnique( - extension.pTextureResource.Get(), - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); + extension.pTextureResource = textureResource; + + return textureResource; } -}; -std::optional getSourceIndexFromModelAndTexture( - const CesiumGltf::Model& model, - const CesiumGltf::Texture& texture) { - const CesiumGltf::ExtensionKhrTextureBasisu* pKtxExtension = - texture.getExtension(); - const CesiumGltf::ExtensionTextureWebp* pWebpExtension = - texture.getExtension(); - - int32_t source = -1; - if (pKtxExtension) { - if (pKtxExtension->source < 0 || - pKtxExtension->source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "KTX texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - pKtxExtension->source); - return std::nullopt; - } - return std::optional(pKtxExtension->source); - } else if (pWebpExtension) { - if (pWebpExtension->source < 0 || - pWebpExtension->source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "WebP texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - pWebpExtension->source); - return std::nullopt; - } - return std::optional(pWebpExtension->source); - } else { - if (texture.source < 0 || texture.source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "Texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - texture.source); - return std::nullopt; + std::optional getSourceIndexFromModelAndTexture( + const CesiumGltf::Model& model, + const CesiumGltf::Texture& texture) { + const CesiumGltf::ExtensionKhrTextureBasisu* pKtxExtension = + texture.getExtension(); + const CesiumGltf::ExtensionTextureWebp* pWebpExtension = + texture.getExtension(); + + int32_t source = -1; + if (pKtxExtension) { + if (pKtxExtension->source < 0 || + pKtxExtension->source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "KTX texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + pKtxExtension->source); + return std::nullopt; + } + return std::optional(pKtxExtension->source); + } else if (pWebpExtension) { + if (pWebpExtension->source < 0 || + pWebpExtension->source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "WebP texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + pWebpExtension->source); + return std::nullopt; + } + return std::optional(pWebpExtension->source); + } else { + if (texture.source < 0 || texture.source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + texture.source); + return std::nullopt; + } + return std::optional(texture.source); } - return std::optional(texture.source); } -} -TUniquePtr loadTextureFromModelAnyThreadPart( - CesiumGltf::Model& model, - CesiumGltf::Texture& texture, - bool sRGB) { - int64_t textureIndex = - model.textures.empty() ? -1 : &texture - &model.textures[0]; - if (textureIndex < 0 || size_t(textureIndex) >= model.textures.size()) { - textureIndex = -1; - } + TUniquePtr loadTextureFromModelAnyThreadPart( + CesiumGltf::Model& model, + CesiumGltf::Texture& texture, + bool sRGB) { + int64_t textureIndex = + model.textures.empty() ? -1 : &texture - &model.textures[0]; + if (textureIndex < 0 || size_t(textureIndex) >= model.textures.size()) { + textureIndex = -1; + } - ExtensionUnrealTexture& extension = - texture.addExtension(); - if (extension.pTexture && (extension.pTexture->getUnrealTexture() || - extension.pTexture->getTextureResource())) { - // There's an existing Unreal texture for this glTF texture. This will - // happen if this texture is used by multiple primitives on the same - // model. It will also be the case when this model was upsampled from a - // parent tile. - TUniquePtr pResult = MakeUnique(); - pResult->pTexture = extension.pTexture; - pResult->textureIndex = textureIndex; - return pResult; - } + ExtensionUnrealTexture& extension = + texture.addExtension(); + if (extension.pTexture && (extension.pTexture->getUnrealTexture() || + extension.pTexture->getTextureResource())) { + // There's an existing Unreal texture for this glTF texture. This will + // happen if this texture is used by multiple primitives on the same + // model. It will also be the case when this model was upsampled from a + // parent tile. + TUniquePtr pResult = + MakeUnique(); + pResult->pTexture = extension.pTexture; + pResult->textureIndex = textureIndex; + return pResult; + } - std::optional optionalSourceIndex = - getSourceIndexFromModelAndTexture(model, texture); - if (!optionalSourceIndex.has_value()) { - return nullptr; - }; + std::optional optionalSourceIndex = + getSourceIndexFromModelAndTexture(model, texture); + if (!optionalSourceIndex.has_value()) { + return nullptr; + }; - CesiumGltf::Image& image = model.images[*optionalSourceIndex]; - const CesiumGltf::Sampler& sampler = - model.getSafe(model.samplers, texture.sampler); + CesiumGltf::Image& image = model.images[*optionalSourceIndex]; + const CesiumGltf::Sampler& sampler = + model.getSafe(model.samplers, texture.sampler); - TUniquePtr result = - loadTextureFromImageAndSamplerAnyThreadPart(image.cesium, sampler, sRGB); + TUniquePtr result = + loadTextureFromImageAndSamplerAnyThreadPart( + image.cesium, + sampler, + sRGB); - if (result) { - extension.pTexture = result->pTexture; - result->textureIndex = textureIndex; - } + if (result) { + extension.pTexture = result->pTexture; + result->textureIndex = textureIndex; + } - return result; -} + return result; + } -TextureFilter getTextureFilterFromSampler(const CesiumGltf::Sampler& sampler) { - // Unreal Engine's available filtering modes are only nearest, bilinear, - // trilinear, and "default". Default means "use the texture group settings", - // and the texture group settings are defined in a config file and can - // vary per platform. All filter modes can use mipmaps if they're available, - // but only TF_Default will ever use anisotropic texture filtering. - // - // Unreal also doesn't separate the minification filter from the - // magnification filter. So we'll just ignore the magFilter unless it's the - // only filter specified. - // - // Generally our bias is toward TF_Default, because that gives the user more - // control via texture groups. - - if (sampler.magFilter && !sampler.minFilter) { - // Only a magnification filter is specified, so use it. - return sampler.magFilter.value() == CesiumGltf::Sampler::MagFilter::NEAREST - ? TextureFilter::TF_Nearest - : TextureFilter::TF_Default; - } else if (sampler.minFilter) { - // Use specified minFilter. - switch (sampler.minFilter.value()) { - case CesiumGltf::Sampler::MinFilter::NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - return TextureFilter::TF_Nearest; - case CesiumGltf::Sampler::MinFilter::LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - return TextureFilter::TF_Bilinear; - default: + TextureFilter + getTextureFilterFromSampler(const CesiumGltf::Sampler& sampler) { + // Unreal Engine's available filtering modes are only nearest, bilinear, + // trilinear, and "default". Default means "use the texture group settings", + // and the texture group settings are defined in a config file and can + // vary per platform. All filter modes can use mipmaps if they're available, + // but only TF_Default will ever use anisotropic texture filtering. + // + // Unreal also doesn't separate the minification filter from the + // magnification filter. So we'll just ignore the magFilter unless it's the + // only filter specified. + // + // Generally our bias is toward TF_Default, because that gives the user more + // control via texture groups. + + if (sampler.magFilter && !sampler.minFilter) { + // Only a magnification filter is specified, so use it. + return sampler.magFilter.value() == + CesiumGltf::Sampler::MagFilter::NEAREST + ? TextureFilter::TF_Nearest + : TextureFilter::TF_Default; + } else if (sampler.minFilter) { + // Use specified minFilter. + switch (sampler.minFilter.value()) { + case CesiumGltf::Sampler::MinFilter::NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return TextureFilter::TF_Nearest; + case CesiumGltf::Sampler::MinFilter::LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + return TextureFilter::TF_Bilinear; + default: + return TextureFilter::TF_Default; + } + } else { + // No filtering specified at all, let the texture group decide. return TextureFilter::TF_Default; } - } else { - // No filtering specified at all, let the texture group decide. - return TextureFilter::TF_Default; } -} - -bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { - switch (sampler.minFilter.value_or( - CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - return true; - break; - default: // LINEAR and NEAREST - return false; - } -} -TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::SharedAsset& image, - const CesiumGltf::Sampler& sampler, - bool sRGB) { - return loadTextureAnyThreadPart( - image, - convertGltfWrapSToUnreal(sampler.wrapS), - convertGltfWrapTToUnreal(sampler.wrapT), - getTextureFilterFromSampler(sampler), - getUseMipmapsIfAvailableFromSampler(sampler), - // TODO: allow texture group to be configured on Cesium3DTileset. - TEXTUREGROUP_World, - sRGB, - std::nullopt); -} - -static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { - if (!pHalfLoadedTexture || !pHalfLoadedTexture->pTexture) { - return nullptr; + bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { + switch (sampler.minFilter.value_or( + CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return true; + break; + default: // LINEAR and NEAREST + return false; + } } - UTexture2D* pTexture = pHalfLoadedTexture->pTexture->getUnrealTexture(); - if (!pTexture) { - pTexture = NewObject( - GetTransientPackage(), - MakeUniqueObjectName( - GetTransientPackage(), - UTexture2D::StaticClass(), - "CesiumRuntimeTexture"), - RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); - - pTexture->AddressX = pHalfLoadedTexture->addressX; - pTexture->AddressY = pHalfLoadedTexture->addressY; - pTexture->Filter = pHalfLoadedTexture->filter; - pTexture->LODGroup = pHalfLoadedTexture->group; - pTexture->SRGB = pHalfLoadedTexture->sRGB; - - pTexture->NeverStream = true; - - pHalfLoadedTexture->pTexture->setUnrealTexture(pTexture); + TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( + CesiumGltf::SharedAsset& image, + const CesiumGltf::Sampler& sampler, + bool sRGB) { + return loadTextureAnyThreadPart( + image, + convertGltfWrapSToUnreal(sampler.wrapS), + convertGltfWrapTToUnreal(sampler.wrapT), + getTextureFilterFromSampler(sampler), + getUseMipmapsIfAvailableFromSampler(sampler), + // TODO: allow texture group to be configured on Cesium3DTileset. + TEXTUREGROUP_World, + sRGB, + std::nullopt); } - return pTexture; -} - -TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::SharedAsset& image, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - std::optional overridePixelFormat) { - TUniquePtr textureResource = - ExtensionUnrealTextureResource::loadTextureResource( - *image, - addressX, - addressY, - filter, - useMipMapsIfAvailable, - group, - sRGB, - overridePixelFormat); + static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { + if (!pHalfLoadedTexture || !pHalfLoadedTexture->pTexture) { + return nullptr; + } - TUniquePtr pResult = MakeUnique(); - pResult->pTexture = new ReferenceCountedUnrealTexture(); + UTexture2D* pTexture = pHalfLoadedTexture->pTexture->getUnrealTexture(); + if (!pTexture) { + pTexture = NewObject( + GetTransientPackage(), + MakeUniqueObjectName( + GetTransientPackage(), + UTexture2D::StaticClass(), + "CesiumRuntimeTexture"), + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + pTexture->AddressX = pHalfLoadedTexture->addressX; + pTexture->AddressY = pHalfLoadedTexture->addressY; + pTexture->Filter = pHalfLoadedTexture->filter; + pTexture->LODGroup = pHalfLoadedTexture->group; + pTexture->SRGB = pHalfLoadedTexture->sRGB; + + pTexture->NeverStream = true; + + pHalfLoadedTexture->pTexture->setUnrealTexture(pTexture); + } - pResult->addressX = addressX; - pResult->addressY = addressY; - pResult->filter = filter; - pResult->group = group; - pResult->sRGB = sRGB; + return pTexture; + } - pResult->pTexture->setTextureResource(MoveTemp(textureResource)); - pResult->pTexture->setSharedImage(image); - return pResult; -} + TUniquePtr loadTextureAnyThreadPart( + CesiumGltf::SharedAsset& image, + TextureAddress addressX, + TextureAddress addressY, + TextureFilter filter, + bool useMipMapsIfAvailable, + TextureGroup group, + bool sRGB, + std::optional overridePixelFormat) { + TSharedPtr textureResource = + ExtensionUnrealTextureResource::loadTextureResource( + *image, + addressX, + addressY, + filter, + useMipMapsIfAvailable, + group, + sRGB, + overridePixelFormat); -CesiumUtility::IntrusivePointer -loadTextureGameThreadPart( - CesiumGltf::Model& model, - LoadedTextureResult* pHalfLoadedTexture) { - if (pHalfLoadedTexture == nullptr) - return nullptr; + TUniquePtr pResult = MakeUnique(); + pResult->pTexture = new ReferenceCountedUnrealTexture(); - CesiumUtility::IntrusivePointer pResult = - loadTextureGameThreadPart(pHalfLoadedTexture); + pResult->addressX = addressX; + pResult->addressY = addressY; + pResult->filter = filter; + pResult->group = group; + pResult->sRGB = sRGB; - if (pResult && pHalfLoadedTexture && pHalfLoadedTexture->textureIndex >= 0 && - size_t(pHalfLoadedTexture->textureIndex) < model.textures.size()) { - CesiumGltf::Texture& texture = - model.textures[pHalfLoadedTexture->textureIndex]; - ExtensionUnrealTexture& extension = - texture.addExtension(); - extension.pTexture = pHalfLoadedTexture->pTexture; + pResult->pTexture->setTextureResource(MoveTemp(textureResource)); + pResult->pTexture->setSharedImage(image); + return pResult; } - return pHalfLoadedTexture->pTexture; -} + CesiumUtility::IntrusivePointer + loadTextureGameThreadPart( + CesiumGltf::Model& model, + LoadedTextureResult* pHalfLoadedTexture) { + if (pHalfLoadedTexture == nullptr) + return nullptr; -CesiumUtility::IntrusivePointer -loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTexture) + CesiumUtility::IntrusivePointer pResult = + loadTextureGameThreadPart(pHalfLoadedTexture); + + if (pResult && pHalfLoadedTexture && + pHalfLoadedTexture->textureIndex >= 0 && + size_t(pHalfLoadedTexture->textureIndex) < model.textures.size()) { + CesiumGltf::Texture& texture = + model.textures[pHalfLoadedTexture->textureIndex]; + ExtensionUnrealTexture& extension = + texture.addExtension(); + extension.pTexture = pHalfLoadedTexture->pTexture; + } - TUniquePtr& pTextureResource = - pHalfLoadedTexture->pTexture->getTextureResource(); - if (pTextureResource == nullptr) { - // Texture is already loaded (or unloadable). return pHalfLoadedTexture->pTexture; } - UTexture2D* pTexture = CreateTexture2D(pHalfLoadedTexture); - if (pTexture == nullptr) { - return nullptr; - } + CesiumUtility::IntrusivePointer + loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTexture) + + TSharedPtr& pTextureResource = + pHalfLoadedTexture->pTexture->getTextureResource(); + if (pTextureResource == nullptr) { + // Texture is already loaded (or unloadable). + return pHalfLoadedTexture->pTexture; + } - FCesiumTextureResourceBase* pCesiumTextureResource = - pTextureResource.Release(); - if (pCesiumTextureResource) { - pTexture->SetResource(pCesiumTextureResource); + UTexture2D* pTexture = CreateTexture2D(pHalfLoadedTexture); + if (pTexture == nullptr) { + return nullptr; + } - ENQUEUE_RENDER_COMMAND(Cesium_InitResource) - ([pTexture, pCesiumTextureResource](FRHICommandListImmediate& RHICmdList) { - pCesiumTextureResource->SetTextureReference( - pTexture->TextureReference.TextureReferenceRHI); + FCesiumTextureResourceBase* pCesiumTextureResource = + pTextureResource.Release(); + if (pCesiumTextureResource) { + pTexture->SetResource(pCesiumTextureResource); + + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture, + pCesiumTextureResource](FRHICommandListImmediate& RHICmdList) { + pCesiumTextureResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); #if ENGINE_VERSION_5_3_OR_HIGHER - pCesiumTextureResource->InitResource( - FRHICommandListImmediate::Get()); // Init Resource now requires a - // command list. + pCesiumTextureResource->InitResource( + FRHICommandListImmediate::Get()); // Init Resource now requires a + // command list. #else - pCesiumTextureResource->InitResource(); + pCesiumTextureResource->InitResource(); #endif - }); - } - - return pHalfLoadedTexture->pTexture; -} + }); + } -TextureAddress convertGltfWrapSToUnreal(int32_t wrapS) { - // glTF spec: "When undefined, a sampler with repeat wrapping and auto - // filtering should be used." - switch (wrapS) { - case CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE: - return TextureAddress::TA_Clamp; - case CesiumGltf::Sampler::WrapS::MIRRORED_REPEAT: - return TextureAddress::TA_Mirror; - case CesiumGltf::Sampler::WrapS::REPEAT: - default: - return TextureAddress::TA_Wrap; + return pHalfLoadedTexture->pTexture; } -} -TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { - // glTF spec: "When undefined, a sampler with repeat wrapping and auto - // filtering should be used." - switch (wrapT) { - case CesiumGltf::Sampler::WrapT::CLAMP_TO_EDGE: - return TextureAddress::TA_Clamp; - case CesiumGltf::Sampler::WrapT::MIRRORED_REPEAT: - return TextureAddress::TA_Mirror; - case CesiumGltf::Sampler::WrapT::REPEAT: - default: - return TextureAddress::TA_Wrap; - } -} + TSharedPtr createTextureResource( + CesiumGltf::ImageCesium& imageCesium, + bool sRGB, + std::optional overridePixelFormat) { + std::optional optionalPixelFormat = + getPixelFormatForImageCesium(imageCesium, overridePixelFormat); + if (!optionalPixelFormat.has_value()) { + return nullptr; + } -std::optional> -createMipMapsForSampler( - const CesiumAsync::AsyncSystem& asyncSystem, - const CesiumGltf::Sampler& sampler, - CesiumGltf::ImageCesium& image) { - std::unique_lock lock(textureResourceMutex); + EPixelFormat pixelFormat = optionalPixelFormat.value(); - ExtensionUnrealTextureResource& extension = - image.addExtension(); + // Store the current size of the pixel data, because + // we're about to clear it but we still want to have + // an accurate estimation of the size of the image for + // caching purposes. + imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); - // Future already exists, we don't need to do anything else. - if (extension.preprocessFuture.has_value()) { - return extension.preprocessFuture.value(); + return createTextureResourceFromImageCesium( + imageCesium, + TextureAddress::TA_Wrap, + TextureAddress::TA_Wrap, + TextureFilter::TF_Default, + true, + TextureGroup::TEXTUREGROUP_World, + sRGB, + pixelFormat); } - // Generate mipmaps if needed. - // An image needs mipmaps generated for it if: - // 1. It is used by a Texture that has a Sampler with a mipmap filtering - // mode, and - // 2. It does not already have mipmaps. - // It's ok if an image has mipmaps even if not all textures will use them. - // There's no reason to have two RHI textures, one with and one without - // mips. - bool needsMipmaps; - switch (sampler.minFilter.value_or( - CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - needsMipmaps = true; - break; - default: // LINEAR and NEAREST - needsMipmaps = false; - break; + TextureAddress convertGltfWrapSToUnreal(int32_t wrapS) { + // glTF spec: "When undefined, a sampler with repeat wrapping and auto + // filtering should be used." + switch (wrapS) { + case CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE: + return TextureAddress::TA_Clamp; + case CesiumGltf::Sampler::WrapS::MIRRORED_REPEAT: + return TextureAddress::TA_Mirror; + case CesiumGltf::Sampler::WrapS::REPEAT: + default: + return TextureAddress::TA_Wrap; + } } - if (!needsMipmaps || image.pixelData.empty()) { - // If we don't need mipmaps, we don't want to create a future for them. - // This allows a future sampler using this image that does need mipmaps to - // generate them. - return std::nullopt; + TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { + // glTF spec: "When undefined, a sampler with repeat wrapping and auto + // filtering should be used." + switch (wrapT) { + case CesiumGltf::Sampler::WrapT::CLAMP_TO_EDGE: + return TextureAddress::TA_Clamp; + case CesiumGltf::Sampler::WrapT::MIRRORED_REPEAT: + return TextureAddress::TA_Mirror; + case CesiumGltf::Sampler::WrapT::REPEAT: + default: + return TextureAddress::TA_Wrap; + } } - CesiumAsync::Promise promise = - asyncSystem.createPromise(); + std::optional> + createMipMapsForSampler( + const CesiumAsync::AsyncSystem& asyncSystem, + const CesiumGltf::Sampler& sampler, + CesiumGltf::ImageCesium& image) { + std::unique_lock lock(textureResourceMutex); - extension.preprocessFuture = promise.getFuture().share(); + ExtensionUnrealTextureResource& extension = + image.addExtension(); - lock.unlock(); + // Future already exists, we don't need to do anything else. + if (extension.preprocessFuture.has_value()) { + return extension.preprocessFuture.value(); + } - // We need mipmaps generated. - std::optional errorMessage = - CesiumGltfReader::GltfReader::generateMipMaps(image); - if (errorMessage) { - UE_LOG( - LogCesium, - Warning, - TEXT("%s"), - UTF8_TO_TCHAR(errorMessage->c_str())); - } - promise.resolve(&image); - return *extension.preprocessFuture; -} + // Generate mipmaps if needed. + // An image needs mipmaps generated for it if: + // 1. It is used by a Texture that has a Sampler with a mipmap filtering + // mode, and + // 2. It does not already have mipmaps. + // It's ok if an image has mipmaps even if not all textures will use them. + // There's no reason to have two RHI textures, one with and one without + // mips. + bool needsMipmaps; + switch (sampler.minFilter.value_or( + CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + needsMipmaps = true; + break; + default: // LINEAR and NEAREST + needsMipmaps = false; + break; + } -CesiumAsync::SharedFuture createMipMapsForAllTextures( - const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::Model& model) { - std::vector> futures; - for (const Texture& texture : model.textures) { - const CesiumGltf::Sampler& sampler = - model.getSafe(model.samplers, texture.sampler); - std::optional> - optionalFuture = createMipMapsForSampler( - asyncSystem, - sampler, - *model.images[texture.source].cesium); - if (optionalFuture.has_value()) { - futures.push_back(optionalFuture.value()); + if (!needsMipmaps || image.pixelData.empty()) { + // If we don't need mipmaps, we don't want to create a future for them. + // This allows a future sampler using this image that does need mipmaps to + // generate them. + return std::nullopt; + } + + CesiumAsync::Promise promise = + asyncSystem.createPromise(); + + extension.preprocessFuture = promise.getFuture().share(); + + lock.unlock(); + + // We need mipmaps generated. + std::optional errorMessage = + CesiumGltfReader::GltfReader::generateMipMaps(image); + if (errorMessage) { + UE_LOG( + LogCesium, + Warning, + TEXT("%s"), + UTF8_TO_TCHAR(errorMessage->c_str())); } + promise.resolve(&image); + return *extension.preprocessFuture; } - return asyncSystem.all(std::move(futures)) - .thenImmediately( - []([[maybe_unused]] std::vector< - CesiumGltf::ImageCesium*>&& /*results*/) -> void {}) - .share(); -} + CesiumAsync::SharedFuture createMipMapsForAllTextures( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::Model& model) { + std::vector> futures; + for (const Texture& texture : model.textures) { + const CesiumGltf::Sampler& sampler = + model.getSafe(model.samplers, texture.sampler); + std::optional> + optionalFuture = createMipMapsForSampler( + asyncSystem, + sampler, + *model.images[texture.source].cesium); + if (optionalFuture.has_value()) { + futures.push_back(optionalFuture.value()); + } + } + + return asyncSystem.all(std::move(futures)) + .thenImmediately( + []([[maybe_unused]] std::vector< + CesiumGltf::ImageCesium*>&& /*results*/) -> void {}) + .share(); + } } // namespace CesiumTextureUtility diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 22cf7b639..767b6da4e 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -47,9 +47,9 @@ struct ReferenceCountedUnrealTexture void setUnrealTexture(const TObjectPtr& p); // The renderer / RHI FTextureResource holding the pixel data. - const TUniquePtr& getTextureResource() const; - TUniquePtr& getTextureResource(); - void setTextureResource(TUniquePtr&& p); + const TSharedPtr& getTextureResource() const; + TSharedPtr& getTextureResource(); + void setTextureResource(TSharedPtr&& p); /// The SharedAsset that this texture was created from. CesiumGltf::SharedAsset getSharedImage() const; @@ -57,7 +57,7 @@ struct ReferenceCountedUnrealTexture private: TObjectPtr _pUnrealTexture; - TUniquePtr _pTextureResource; + TSharedPtr _pTextureResource; CesiumGltf::SharedAsset _pImageCesium; }; @@ -186,6 +186,16 @@ loadTextureGameThreadPart( CesiumUtility::IntrusivePointer loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture); +/** + * Creates a `FCesiumTextureResourceBase` for an image. This texture resource is + * intended to be later used with `FCesiumUseExistingTextureResource`, which + * will supply sampler, texture group, and other settings. + */ +TSharedPtr createTextureResource( + CesiumGltf::ImageCesium& imageCesium, + bool sRGB, + std::optional overridePixelFormat); + /** * @brief Convert a glTF {@link CesiumGltf::Sampler::WrapS} value to an Unreal * `TextureAddress` value. diff --git a/extern/cesium-native b/extern/cesium-native index 31d0048b5..e78188244 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 31d0048b58044546182d2bcd9d72106bb5ca8991 +Subproject commit e7818824412a68c3e6027b9e131cb39a33bed15b From 8b0bcdc8cfc77093421210a609c4822fd209389f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 14:56:09 +1000 Subject: [PATCH 02/23] Sort of working again after reorg. But raster overlays don't work, and there appears to be an sRGB problem. --- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 4 +- .../Private/CesiumGltfComponent.cpp | 17 +- .../Private/CesiumGltfTextures.cpp | 70 +- .../Private/CesiumGltfTextures.h | 2 +- .../Private/CesiumTextureResource.cpp | 351 ++++++- .../Private/CesiumTextureResource.h | 142 ++- .../Private/CesiumTextureUtility.cpp | 944 +++++++----------- .../Private/CesiumTextureUtility.h | 21 +- .../Private/ExtensionImageCesiumUnreal.cpp | 101 ++ .../Private/ExtensionImageCesiumUnreal.h | 68 ++ extern/cesium-native | 2 +- 11 files changed, 943 insertions(+), 779 deletions(-) create mode 100644 Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp create mode 100644 Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 39b58fa61..af8a85824 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -948,7 +948,7 @@ class UnrealResourcePreparer const Cesium3DTilesSelection::TileContent& content = tile.getContent(); const Cesium3DTilesSelection::TileRenderContent* pRenderContent = content.getRenderContent(); - if (pRenderContent) { + if (pMainThreadRendererResources != nullptr && pRenderContent != nullptr) { UCesiumGltfComponent* pGltfContent = reinterpret_cast( pRenderContent->getRenderResources()); @@ -978,7 +978,7 @@ class UnrealResourcePreparer UCesiumGltfComponent* pGltfContent = reinterpret_cast( pRenderContent->getRenderResources()); - if (pGltfContent) { + if (pMainThreadRendererResources != nullptr && pGltfContent != nullptr) { pGltfContent->DetachRasterTile( tile, rasterTile, diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 2f07cd875..e8d2c5620 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -8,6 +8,7 @@ #include "CesiumFeatureIdSet.h" #include "CesiumGltfPointsComponent.h" #include "CesiumGltfPrimitiveComponent.h" +#include "CesiumGltfTextures.h" #include "CesiumMaterialUserData.h" #include "CesiumRasterOverlays.h" #include "CesiumRuntime.h" @@ -400,22 +401,22 @@ struct ColorVisitor { template static TUniquePtr loadTexture( CesiumGltf::Model& model, - const std::optional& gltfTexture, + const std::optional& gltfTextureInfo, bool sRGB) { - if (!gltfTexture || gltfTexture.value().index < 0 || - gltfTexture.value().index >= model.textures.size()) { - if (gltfTexture && gltfTexture.value().index >= 0) { + if (!gltfTextureInfo || gltfTextureInfo.value().index < 0 || + gltfTextureInfo.value().index >= model.textures.size()) { + if (gltfTextureInfo && gltfTextureInfo.value().index >= 0) { UE_LOG( LogCesium, Warning, TEXT("Texture index must be less than %d, but is %d"), model.textures.size(), - gltfTexture.value().index); + gltfTextureInfo.value().index); } return nullptr; } - int32_t textureIndex = gltfTexture.value().index; + int32_t textureIndex = gltfTextureInfo.value().index; CesiumGltf::Texture& texture = model.textures[textureIndex]; return loadTextureFromModelAnyThreadPart(model, texture, sRGB); } @@ -1061,7 +1062,7 @@ std::string constrainLength(const std::string& s, const size_t maxLength) { } /** - * @brief Create an FName from the given strings. + * @brief CreateNew an FName from the given strings. * * This will combine the prefix and the suffix and create an FName. * If the string would be longer than the given length, then @@ -2238,7 +2239,7 @@ loadModelAnyThreadPart( const CesiumGeospatial::Ellipsoid& ellipsoid) { TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::loadModelAnyThreadPart) - return createMipMapsForAllTextures(asyncSystem, *options.pModel) + return CesiumGltfTextures::createInWorkerThread(asyncSystem, *options.pModel) .thenInWorkerThread( [transform, ellipsoid, options = std::move(options)]() -> UCesiumGltfComponent::CreateOffGameThreadResult { diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index a6fff0bbb..95cfe003c 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -4,6 +4,7 @@ #include "CesiumRuntime.h" #include "CesiumTextureResource.h" #include "CesiumTextureUtility.h" +#include "ExtensionImageCesiumUnreal.h" #include #include #include @@ -36,7 +37,7 @@ SharedFuture createTextureInLoadThread( } // namespace -/*static*/ CesiumAsync::Future CesiumGltfTextures::createInLoadThread( +/*static*/ CesiumAsync::Future CesiumGltfTextures::createInWorkerThread( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::Model& model) { // This array is parallel to model.images and indicates whether each image @@ -183,39 +184,6 @@ bool doesTextureUseMipmaps(const Model& gltf, const Texture& texture) { } } -struct ExtensionUnrealTextureResource { - static inline constexpr const char* TypeName = - "ExtensionUnrealTextureResource"; - static inline constexpr const char* ExtensionName = - "PRIVATE_unreal_texture_resource"; - - ExtensionUnrealTextureResource() {} - - TSharedPtr pTextureResource = nullptr; - std::optional> createFuture = std::nullopt; -}; - -std::mutex textureResourceMutex; - -// Returns a Future that will resolve when the image is loaded. It _may_ also -// return a Promise, in which case the calling thread is responsible for doing -// the loading and should resolve the Promise when it's done. -std::pair, std::optional>> -getOrCreateImageFuture(const AsyncSystem& asyncSystem, Image& image) { - std::scoped_lock lock(textureResourceMutex); - - ExtensionUnrealTextureResource& extension = - image.cesium->addExtension(); - if (extension.createFuture) { - // Another thread is already working on this image. - return {*extension.createFuture, std::nullopt}; - } else { - // This thread will work on this image. - Promise promise = asyncSystem.createPromise(); - return {promise.getFuture().share(), promise}; - } -} - SharedFuture createTextureInLoadThread( const AsyncSystem& asyncSystem, Model& gltf, @@ -233,35 +201,13 @@ SharedFuture createTextureInLoadThread( check(pTexture->source >= 0 && pTexture->source < imageNeedsMipmaps.size()); bool needsMips = imageNeedsMipmaps[pTexture->source]; - auto [future, maybePromise] = getOrCreateImageFuture(asyncSystem, *pImage); - if (!maybePromise) { - // Another thread is already loading this image. - return future; - } - - // Proceed to load the image in this thread. - ImageCesium& cesium = *pImage->cesium; - - if (needsMips && !cesium.pixelData.empty()) { - std::optional errorMessage = - CesiumGltfReader::GltfReader::generateMipMaps(cesium); - if (errorMessage) { - UE_LOG( - LogCesium, - Warning, - TEXT("%s"), - UTF8_TO_TCHAR(errorMessage->c_str())); - } - } - - CesiumTextureUtility::loadTextureFromModelAnyThreadPart( - gltf, - *pTexture, - sRGB); - - maybePromise->resolve(); + const ExtensionImageCesiumUnreal& extension = ExtensionImageCesiumUnreal::GetOrCreate( + asyncSystem, + *pImage->cesium, + needsMips, + std::nullopt); - return future; + return extension.getFuture(); } } // namespace diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.h b/Source/CesiumRuntime/Private/CesiumGltfTextures.h index 339481b1b..7f0326ad1 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.h +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.h @@ -18,7 +18,7 @@ class CesiumGltfTextures { * Creates all of the textures that are required by the given glTF, and adds * `ExtensionUnrealTexture` to each. */ - static CesiumAsync::Future createInLoadThread( + static CesiumAsync::Future createInWorkerThread( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::Model& model); }; diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index 1bbeb5a59..b3ca34a8b 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp @@ -1,11 +1,87 @@ // Copyright 2020-2024 CesiumGS, Inc. and Contributors #include "CesiumTextureResource.h" +#include "CesiumRuntime.h" +#include "CesiumTextureUtility.h" #include "Misc/CoreStats.h" #include "RenderUtils.h" +#include namespace { +/** + * A Cesium texture resource that uses an already-created `FRHITexture`. This is + * used when `GRHISupportsAsyncTextureCreation` is true and so we were already + * able to create the FRHITexture in a worker thread. It is also used when a + * single glTF `Image` is referenced by multiple glTF `Texture` instances. We + * only need one `FRHITexture` is this case, but we need multiple + * `FTextureResource` instances to support the different sampler settings that + * are likely used in the different textures. + */ +class FCesiumUseExistingTextureResource : public FCesiumTextureResource { +public: + FCesiumUseExistingTextureResource( + FTextureRHIRef existingTexture, + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipsIfAvailable, + uint32 extData); + + FCesiumUseExistingTextureResource( + const TSharedPtr& pExistingTexture, + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipsIfAvailable, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; + +private: + TSharedPtr _pExistingTexture; +}; + +/** + * A Cesium texture resource that creates an `FRHITexture` from a glTF + * `ImageCesium` when `InitRHI` is called from the render thread. When + * `GRHISupportsAsyncTextureCreation` is false (everywhere but Direct3D), we can + * only create a `FRHITexture` on the render thread, so this is the code that + * does it. + */ +class FCesiumCreateNewTextureResource : public FCesiumTextureResource { +public: + FCesiumCreateNewTextureResource( + CesiumGltf::ImageCesium&& image, + TextureGroup textureGroup, + uint32 width, + uint32 height, + EPixelFormat format, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipsIfAvailable, + uint32 extData); + +protected: + virtual FTextureRHIRef InitializeTextureRHI() override; + +private: + CesiumGltf::ImageCesium _image; +}; + ESamplerFilter convertFilter(TextureFilter filter) { switch (filter) { case TF_Nearest: @@ -98,9 +174,268 @@ void CopyMip( } } +FTexture2DRHIRef createAsyncTextureAndWait( + uint32 SizeX, + uint32 SizeY, + uint8 Format, + uint32 NumMips, + ETextureCreateFlags Flags, + void** InitialMipData, + uint32 NumInitialMips) { +#if ENGINE_VERSION_5_4_OR_HIGHER + FGraphEventRef CompletionEvent; + + FTexture2DRHIRef result = RHIAsyncCreateTexture2D( + SizeX, + SizeY, + Format, + NumMips, + Flags, + ERHIAccess::Unknown, + InitialMipData, + NumInitialMips, + TEXT("CesiumTexture"), + CompletionEvent); + + if (CompletionEvent) { + CompletionEvent->Wait(); + } + + return result; +#elif ENGINE_VERSION_5_3_OR_HIGHER + FGraphEventRef CompletionEvent; + + FTexture2DRHIRef result = RHIAsyncCreateTexture2D( + SizeX, + SizeY, + Format, + NumMips, + Flags, + InitialMipData, + NumInitialMips, + CompletionEvent); + + if (CompletionEvent) { + CompletionEvent->Wait(); + } + + return result; +#else + return RHIAsyncCreateTexture2D( + SizeX, + SizeY, + Format, + NumMips, + Flags, + InitialMipData, + NumInitialMips); +#endif +} + +/** + * @brief Create an RHI texture on this thread. This requires + * GRHISupportsAsyncTextureCreation to be true. + * + * @param image The CPU image to create on the GPU. + * @param format The pixel format of the image. + * @param Whether to use a sRGB color-space. + * @return The RHI texture reference. + */ +FTexture2DRHIRef CreateRHITexture2D_Async( + const CesiumGltf::ImageCesium& image, + EPixelFormat format, + bool sRGB) { + check(GRHISupportsAsyncTextureCreation); + + ETextureCreateFlags textureFlags = TexCreate_ShaderResource; + + // Just like in FCesiumCreateNewTextureResource, we're assuming here that we + // can create an FRHITexture as sRGB, and later create another + // UTexture2D / FTextureResource pointing to the same FRHITexture that is not + // sRGB (or vice-versa), and that Unreal will effectively ignore the flag on + // FRHITexture. + if (sRGB) { + textureFlags |= TexCreate_SRGB; + } + + if (!image.mipPositions.empty()) { + // Here 16 is a generously large (but arbitrary) hard limit for number of + // mips. + uint32 mipCount = static_cast(image.mipPositions.size()); + if (mipCount > 16) { + mipCount = 16; + } + + void* mipsData[16]; + for (size_t i = 0; i < mipCount; ++i) { + const CesiumGltf::ImageCesiumMipPosition& mipPos = image.mipPositions[i]; + mipsData[i] = (void*)(&image.pixelData[mipPos.byteOffset]); + } + + return createAsyncTextureAndWait( + static_cast(image.width), + static_cast(image.height), + format, + mipCount, + textureFlags, + mipsData, + mipCount); + } else { + void* pTextureData = (void*)(image.pixelData.data()); + return createAsyncTextureAndWait( + static_cast(image.width), + static_cast(image.height), + format, + 1, + textureFlags, + &pTextureData, + 1); + } +} + } // namespace -FCesiumTextureResourceBase::FCesiumTextureResourceBase( +void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { + FCesiumTextureResource::Destroy(p); +} + +/*static*/ FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateNew( + CesiumGltf::ImageCesium& imageCesium, + TextureGroup textureGroup, + const std::optional& overridePixelFormat, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool needsMipMaps) { + if (imageCesium.pixelData.empty()) { + return nullptr; + } + + if (needsMipMaps) { + std::optional errorMessage = + CesiumGltfReader::GltfReader::generateMipMaps(imageCesium); + if (errorMessage) { + UE_LOG( + LogCesium, + Warning, + TEXT("%s"), + UTF8_TO_TCHAR(errorMessage->c_str())); + } + } + + std::optional maybePixelFormat = + CesiumTextureUtility::getPixelFormatForImageCesium( + imageCesium, + overridePixelFormat); + if (!maybePixelFormat) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Image cannot be created because it has an unsupported compressed pixel format (%d)."), + imageCesium.compressedPixelFormat); + return nullptr; + } + + // Store the current size of the pixel data, because + // we're about to clear it but we still want to have + // an accurate estimation of the size of the image for + // caching purposes. + imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); + + if (GRHISupportsAsyncTextureCreation) { + // Create RHI texture resource on this worker + // thread, and then hand it off to the renderer + // thread. + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::CreateRHITexture2D) + + FTexture2DRHIRef textureReference = + CreateRHITexture2D_Async(imageCesium, *maybePixelFormat, sRGB); + auto pResult = TUniquePtr< + FCesiumUseExistingTextureResource, + FCesiumTextureResourceDeleter>(new FCesiumUseExistingTextureResource( + textureReference, + textureGroup, + imageCesium.width, + imageCesium.height, + *maybePixelFormat, + filter, + addressX, + addressY, + sRGB, + needsMipMaps, + 0)); + + // Clear the now-unnecessary copy of the pixel data. + // Calling clear() isn't good enough because it + // won't actually release the memory. + std::vector pixelData; + imageCesium.pixelData.swap(pixelData); + + std::vector mipPositions; + imageCesium.mipPositions.swap(mipPositions); + + return pResult; + } else { + // The RHI texture will be created later on the + // render thread, directly from this texture source. + // We need valid pixelData here, though. + auto pResult = TUniquePtr< + FCesiumCreateNewTextureResource, + FCesiumTextureResourceDeleter>(new FCesiumCreateNewTextureResource( + std::move(imageCesium), + textureGroup, + imageCesium.width, + imageCesium.height, + *maybePixelFormat, + filter, + addressX, + addressY, + sRGB, + needsMipMaps, + 0)); + return pResult; + } +} + +FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateWrapped( + const TSharedPtr& pExistingResource, + TextureGroup textureGroup, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipMapsIfAvailable) { + if (pExistingResource == nullptr) + return nullptr; + + return FCesiumTextureResourceUniquePtr(new FCesiumUseExistingTextureResource( + pExistingResource, + textureGroup, + pExistingResource->_width, + pExistingResource->_height, + pExistingResource->_format, + filter, + addressX, + addressY, + sRGB, + useMipMapsIfAvailable, + 0)); +} + +/*static*/ void FCesiumTextureResource::Destroy(FCesiumTextureResource* p) { + if (p == nullptr) + return; + + ENQUEUE_RENDER_COMMAND(DeleteResource) + ([p](FRHICommandListImmediate& RHICmdList) { + p->ReleaseResource(); + delete p; + }); +} + +FCesiumTextureResource::FCesiumTextureResource( TextureGroup textureGroup, uint32 width, uint32 height, @@ -128,7 +463,7 @@ FCesiumTextureResourceBase::FCesiumTextureResourceBase( #if ENGINE_VERSION_5_3_OR_HIGHER void FCesiumTextureResourceBase::InitRHI(FRHICommandListBase& RHICmdList) { #else -void FCesiumTextureResourceBase::InitRHI() { +void FCesiumTextureResource::InitRHI() { #endif FSamplerStateInitializerRHI samplerStateInitializer( this->_filter, @@ -188,7 +523,7 @@ void FCesiumTextureResourceBase::InitRHI() { #endif } -void FCesiumTextureResourceBase::ReleaseRHI() { +void FCesiumTextureResource::ReleaseRHI() { DEC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); DEC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); @@ -216,7 +551,7 @@ FOREACH_ENUM_TEXTUREGROUP(DECLARETEXTUREGROUPSTAT) #undef DECLARETEXTUREGROUPSTAT } // namespace -FName FCesiumTextureResourceBase::TextureGroupStatFNames[TEXTUREGROUP_MAX] = { +FName FCesiumTextureResource::TextureGroupStatFNames[TEXTUREGROUP_MAX] = { #define ASSIGNTEXTUREGROUPSTATNAME(Group) GET_STATFNAME(STAT_##Group), FOREACH_ENUM_TEXTUREGROUP(ASSIGNTEXTUREGROUPSTATNAME) #undef ASSIGNTEXTUREGROUPSTATNAME @@ -236,7 +571,7 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( bool sRGB, bool useMipsIfAvailable, uint32 extData) - : FCesiumTextureResourceBase( + : FCesiumTextureResource( textureGroup, width, height, @@ -252,7 +587,7 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( } FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( - FTextureResource* pExistingTexture, + const TSharedPtr& pExistingTexture, TextureGroup textureGroup, uint32 width, uint32 height, @@ -263,7 +598,7 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( bool sRGB, bool useMipsIfAvailable, uint32 extData) - : FCesiumTextureResourceBase( + : FCesiumTextureResource( textureGroup, width, height, @@ -296,7 +631,7 @@ FCesiumCreateNewTextureResource::FCesiumCreateNewTextureResource( bool sRGB, bool useMipsIfAvailable, uint32 extData) - : FCesiumTextureResourceBase( + : FCesiumTextureResource( textureGroup, width, height, diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.h b/Source/CesiumRuntime/Private/CesiumTextureResource.h index 83b75d503..cba96bc6b 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.h +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.h @@ -8,14 +8,79 @@ #include #include +class FCesiumTextureResource; + +struct FCesiumTextureResourceDeleter { + void operator()(FCesiumTextureResource* p); +}; + +using FCesiumTextureResourceUniquePtr = + TUniquePtr; + /** * The base class for Cesium texture resources, making Cesium's texture data * available to Unreal's RHI. The actual creation of the RHI texture is deferred * to a pure virtual method, `InitializeTextureRHI`. */ -class FCesiumTextureResourceBase : public FTextureResource { +class FCesiumTextureResource : public FTextureResource { public: - FCesiumTextureResourceBase( + /** + * Create a new FCesiumTextureResource from an ImageCesium and the given + * sampling parameters. This method is intended to be called from a worker + * thread, not from the game or render thread. + * + * @param imageCesium The image data from which to create the texture + * resource. After this method returns, the `pixelData` will be empty, and + * `sizeBytes` will be set to its previous size. + * @param textureGroup The texture group in which to create this texture. + * @param overridePixelFormat Overrides the pixel format. If std::nullopt, the + * format is inferred from the `ImageCesium`. + * @param filter The texture filtering to use when sampling this texture. + * @param addressX The X texture addressing mode to use when sampling this + * texture. + * @param addressY The Y texture addressing mode to use when sampling this + * texture. + * @param sRGB True if the image data stored in this texture should be treated + * as sRGB. + * @param needsMipMaps True if this texture requires mipmaps. They will be + * generated if they don't already exist. + * @return The created texture resource, or nullptr if a texture could not be + * created. + */ + static FCesiumTextureResourceUniquePtr CreateNew( + CesiumGltf::ImageCesium& imageCesium, + TextureGroup textureGroup, + const std::optional& overridePixelFormat, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool needsMipMaps); + + /** + * Create a new FCesiumTextureResource wrapping an existing one and providing + * new sampling parameters. This method is intended to be called from a worker + * thread, not from the game or render thread. + */ + static FCesiumTextureResourceUniquePtr CreateWrapped( + const TSharedPtr& pExistingResource, + TextureGroup textureGroup, + TextureFilter filter, + TextureAddress addressX, + TextureAddress addressY, + bool sRGB, + bool useMipMapsIfAvailable); + + /** + * Destroys an FCesiumTextureResource. Unreal TextureResources must be + * destroyed on the render thread, so it is important not to call `delete` + * directly. + * + * \param p + */ + static void Destroy(FCesiumTextureResource* p); + + FCesiumTextureResource( TextureGroup textureGroup, uint32 width, uint32 height, @@ -56,76 +121,3 @@ class FCesiumTextureResourceBase : public FTextureResource { FName _lodGroupStatName; uint64 _textureSize; }; - -/** - * A Cesium texture resource that uses an already-created `FRHITexture`. This is - * used when `GRHISupportsAsyncTextureCreation` is true and so we were already - * able to create the FRHITexture in a worker thread. It is also used when a - * single glTF `Image` is referenced by multiple glTF `Texture` instances. We - * only need one `FRHITexture` is this case, but we need multiple - * `FTextureResource` instances to support the different sampler settings that - * are likely used in the different textures. - */ -class FCesiumUseExistingTextureResource : public FCesiumTextureResourceBase { -public: - FCesiumUseExistingTextureResource( - FTextureRHIRef existingTexture, - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - bool useMipsIfAvailable, - uint32 extData); - - FCesiumUseExistingTextureResource( - FTextureResource* pExistingTexture, - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - bool useMipsIfAvailable, - uint32 extData); - -protected: - virtual FTextureRHIRef InitializeTextureRHI() override; - -private: - FTextureResource* _pExistingTexture; -}; - -/** - * A Cesium texture resource that creates an `FRHITexture` from a glTF - * `ImageCesium` when `InitRHI` is called from the render thread. When - * `GRHISupportsAsyncTextureCreation` is false (everywhere but Direct3D), we can - * only create a `FRHITexture` on the render thread, so this is the code that - * does it. - */ -class FCesiumCreateNewTextureResource : public FCesiumTextureResourceBase { -public: - FCesiumCreateNewTextureResource( - CesiumGltf::ImageCesium&& image, - TextureGroup textureGroup, - uint32 width, - uint32 height, - EPixelFormat format, - TextureFilter filter, - TextureAddress addressX, - TextureAddress addressY, - bool sRGB, - bool useMipsIfAvailable, - uint32 extData); - -protected: - virtual FTextureRHIRef InitializeTextureRHI() override; - -private: - CesiumGltf::ImageCesium _image; -}; diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index 19c3ea3f6..edad7fd09 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -10,6 +10,7 @@ #include "CesiumTextureResource.h" #include "Containers/ResourceArray.h" #include "DynamicRHI.h" +#include "ExtensionImageCesiumUnreal.h" #include "GenericPlatform/GenericPlatformProcess.h" #include "PixelFormat.h" #include "RHICommandList.h" @@ -40,69 +41,7 @@ struct ExtensionUnrealTexture { pTexture = nullptr; }; -std::optional getPixelFormatForImageCesium( - const ImageCesium& imageCesium, - const std::optional overridePixelFormat) { - if (imageCesium.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { - switch (imageCesium.compressedPixelFormat) { - case GpuCompressedPixelFormat::ETC1_RGB: - return EPixelFormat::PF_ETC1; - break; - case GpuCompressedPixelFormat::ETC2_RGBA: - return EPixelFormat::PF_ETC2_RGBA; - break; - case GpuCompressedPixelFormat::BC1_RGB: - return EPixelFormat::PF_DXT1; - break; - case GpuCompressedPixelFormat::BC3_RGBA: - return EPixelFormat::PF_DXT5; - break; - case GpuCompressedPixelFormat::BC4_R: - return EPixelFormat::PF_BC4; - break; - case GpuCompressedPixelFormat::BC5_RG: - return EPixelFormat::PF_BC5; - break; - case GpuCompressedPixelFormat::BC7_RGBA: - return EPixelFormat::PF_BC7; - break; - case GpuCompressedPixelFormat::ASTC_4x4_RGBA: - return EPixelFormat::PF_ASTC_4x4; - break; - case GpuCompressedPixelFormat::PVRTC2_4_RGBA: - return EPixelFormat::PF_PVRTC2; - break; - case GpuCompressedPixelFormat::ETC2_EAC_R11: - return EPixelFormat::PF_ETC2_R11_EAC; - break; - case GpuCompressedPixelFormat::ETC2_EAC_RG11: - return EPixelFormat::PF_ETC2_RG11_EAC; - break; - default: - // Unsupported compressed texture format. - return std::nullopt; - }; - } else if (overridePixelFormat) { - return *overridePixelFormat; - } else { - switch (imageCesium.channels) { - case 1: - return PF_R8; - break; - case 2: - return PF_R8G8; - break; - case 3: - case 4: - default: - return PF_R8G8B8A8; - }; - } - - return std::nullopt; -} - -FTexture2DRHIRef createAsyncTextureAndWait( +FTexture2DRHIRef createAsyncTextureAndWaitOld( uint32 SizeX, uint32 SizeY, uint8 Format, @@ -169,7 +108,7 @@ FTexture2DRHIRef createAsyncTextureAndWait( * @param Whether to use a sRGB color-space. * @return The RHI texture reference. */ -FTexture2DRHIRef CreateRHITexture2D_Async( +FTexture2DRHIRef CreateRHITexture2D_AsyncOld( const CesiumGltf::ImageCesium& image, EPixelFormat format, bool sRGB) { @@ -200,7 +139,7 @@ FTexture2DRHIRef CreateRHITexture2D_Async( mipsData[i] = (void*)(&image.pixelData[mipPos.byteOffset]); } - return createAsyncTextureAndWait( + return createAsyncTextureAndWaitOld( static_cast(image.width), static_cast(image.height), format, @@ -210,7 +149,7 @@ FTexture2DRHIRef CreateRHITexture2D_Async( mipCount); } else { void* pTextureData = (void*)(image.pixelData.data()); - return createAsyncTextureAndWait( + return createAsyncTextureAndWaitOld( static_cast(image.width), static_cast(image.height), format, @@ -265,612 +204,399 @@ void ReferenceCountedUnrealTexture::setUnrealTexture( this->_pUnrealTexture = p; } -const TSharedPtr& +const FCesiumTextureResourceUniquePtr& ReferenceCountedUnrealTexture::getTextureResource() const { return this->_pTextureResource; } -TSharedPtr& +FCesiumTextureResourceUniquePtr& ReferenceCountedUnrealTexture::getTextureResource() { return this->_pTextureResource; } void ReferenceCountedUnrealTexture::setTextureResource( - TSharedPtr&& p) { + FCesiumTextureResourceUniquePtr&& p) { this->_pTextureResource = std::move(p); } -CesiumGltf::SharedAsset -ReferenceCountedUnrealTexture::getSharedImage() const { - return this->_pImageCesium; -} - -void ReferenceCountedUnrealTexture::setSharedImage( - CesiumGltf::SharedAsset& image) { - this->_pImageCesium = image; -} - -TSharedPtr createTextureResourceFromImageCesium( - CesiumGltf::ImageCesium& imageCesium, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - EPixelFormat pixelFormat) { - if (GRHISupportsAsyncTextureCreation && !imageCesium.pixelData.empty()) { - // Create RHI texture resource on this worker - // thread, and then hand it off to the renderer - // thread. - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::CreateRHITexture2D) - - FTexture2DRHIRef textureReference = - CreateRHITexture2D_Async(imageCesium, pixelFormat, sRGB); - TSharedPtr textureResource = - MakeShared( - textureReference, - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); - - // Clear the now-unnecessary copy of the pixel data. - // Calling clear() isn't good enough because it - // won't actually release the memory. - std::vector pixelData; - imageCesium.pixelData.swap(pixelData); - - std::vector mipPositions; - imageCesium.mipPositions.swap(mipPositions); - - return textureResource; +std::optional getSourceIndexFromModelAndTexture( + const CesiumGltf::Model& model, + const CesiumGltf::Texture& texture) { + const CesiumGltf::ExtensionKhrTextureBasisu* pKtxExtension = + texture.getExtension(); + const CesiumGltf::ExtensionTextureWebp* pWebpExtension = + texture.getExtension(); + + int32_t source = -1; + if (pKtxExtension) { + if (pKtxExtension->source < 0 || + pKtxExtension->source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "KTX texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + pKtxExtension->source); + return std::nullopt; + } + return std::optional(pKtxExtension->source); + } else if (pWebpExtension) { + if (pWebpExtension->source < 0 || + pWebpExtension->source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "WebP texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + pWebpExtension->source); + return std::nullopt; + } + return std::optional(pWebpExtension->source); } else { - // The RHI texture will be created later on the - // render thread, directly from this texture source. - // We need valid pixelData here, though. - if (imageCesium.pixelData.empty()) { - return nullptr; + if (texture.source < 0 || texture.source >= model.images.size()) { + UE_LOG( + LogCesium, + Warning, + TEXT( + "Texture source index must be non-negative and less than %d, but is %d"), + model.images.size(), + texture.source); + return std::nullopt; } - - return MakeShared( - std::move(imageCesium), - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); + return std::optional(texture.source); } } -static std::mutex textureResourceMutex; - -struct ExtensionUnrealTextureResource { - static inline constexpr const char* TypeName = - "ExtensionUnrealTextureResource"; - static inline constexpr const char* ExtensionName = - "PRIVATE_unreal_texture_resource"; - - ExtensionUnrealTextureResource() {} - - TSharedPtr pTextureResource = nullptr; - - // If a preprocessing step is required (such as generating mipmaps), this - // future returns the preprocessed image. If no preprocessing is required, - // this just passes the image through. - std::optional> - preprocessFuture = std::nullopt; - - static TSharedPtr loadTextureResource( - CesiumGltf::ImageCesium& imageCesium, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - std::optional overridePixelFormat) { - std::optional optionalPixelFormat = - getPixelFormatForImageCesium(imageCesium, overridePixelFormat); - if (!optionalPixelFormat.has_value()) { - return nullptr; - } - - EPixelFormat pixelFormat = optionalPixelFormat.value(); - - std::lock_guard lock(textureResourceMutex); - - ExtensionUnrealTextureResource& extension = - imageCesium.addExtension(); - - // Already have a texture resource, just use that. - if (extension.pTextureResource != nullptr) { - return MakeShared( - extension.pTextureResource.Get(), - group, - imageCesium.width, - imageCesium.height, - pixelFormat, - filter, - addressX, - addressY, - sRGB, - useMipMapsIfAvailable, - 0); - } - - // Store the current size of the pixel data, because - // we're about to clear it but we still want to have - // an accurate estimation of the size of the image for - // caching purposes. - imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); - - TSharedPtr textureResource = - createTextureResourceFromImageCesium( - imageCesium, - addressX, - addressY, - filter, - useMipMapsIfAvailable, - group, - sRGB, - pixelFormat); - - check(textureResource != nullptr); - - extension.pTextureResource = textureResource; - - return textureResource; +TUniquePtr loadTextureFromModelAnyThreadPart( + CesiumGltf::Model& model, + CesiumGltf::Texture& texture, + bool sRGB) { + int64_t textureIndex = + model.textures.empty() ? -1 : &texture - &model.textures[0]; + if (textureIndex < 0 || size_t(textureIndex) >= model.textures.size()) { + textureIndex = -1; } - std::optional getSourceIndexFromModelAndTexture( - const CesiumGltf::Model& model, - const CesiumGltf::Texture& texture) { - const CesiumGltf::ExtensionKhrTextureBasisu* pKtxExtension = - texture.getExtension(); - const CesiumGltf::ExtensionTextureWebp* pWebpExtension = - texture.getExtension(); - - int32_t source = -1; - if (pKtxExtension) { - if (pKtxExtension->source < 0 || - pKtxExtension->source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "KTX texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - pKtxExtension->source); - return std::nullopt; - } - return std::optional(pKtxExtension->source); - } else if (pWebpExtension) { - if (pWebpExtension->source < 0 || - pWebpExtension->source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "WebP texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - pWebpExtension->source); - return std::nullopt; - } - return std::optional(pWebpExtension->source); - } else { - if (texture.source < 0 || texture.source >= model.images.size()) { - UE_LOG( - LogCesium, - Warning, - TEXT( - "Texture source index must be non-negative and less than %d, but is %d"), - model.images.size(), - texture.source); - return std::nullopt; - } - return std::optional(texture.source); - } + ExtensionUnrealTexture& extension = + texture.addExtension(); + if (extension.pTexture && (extension.pTexture->getUnrealTexture() || + extension.pTexture->getTextureResource())) { + // There's an existing Unreal texture for this glTF texture. This will + // happen if this texture is used by multiple primitives on the same + // model. It will also be the case when this model was upsampled from a + // parent tile. + TUniquePtr pResult = MakeUnique(); + pResult->pTexture = extension.pTexture; + pResult->textureIndex = textureIndex; + return pResult; } - TUniquePtr loadTextureFromModelAnyThreadPart( - CesiumGltf::Model& model, - CesiumGltf::Texture& texture, - bool sRGB) { - int64_t textureIndex = - model.textures.empty() ? -1 : &texture - &model.textures[0]; - if (textureIndex < 0 || size_t(textureIndex) >= model.textures.size()) { - textureIndex = -1; - } - - ExtensionUnrealTexture& extension = - texture.addExtension(); - if (extension.pTexture && (extension.pTexture->getUnrealTexture() || - extension.pTexture->getTextureResource())) { - // There's an existing Unreal texture for this glTF texture. This will - // happen if this texture is used by multiple primitives on the same - // model. It will also be the case when this model was upsampled from a - // parent tile. - TUniquePtr pResult = - MakeUnique(); - pResult->pTexture = extension.pTexture; - pResult->textureIndex = textureIndex; - return pResult; - } - - std::optional optionalSourceIndex = - getSourceIndexFromModelAndTexture(model, texture); - if (!optionalSourceIndex.has_value()) { - return nullptr; - }; - - CesiumGltf::Image& image = model.images[*optionalSourceIndex]; - const CesiumGltf::Sampler& sampler = - model.getSafe(model.samplers, texture.sampler); + std::optional optionalSourceIndex = + getSourceIndexFromModelAndTexture(model, texture); + if (!optionalSourceIndex.has_value()) { + return nullptr; + }; - TUniquePtr result = - loadTextureFromImageAndSamplerAnyThreadPart( - image.cesium, - sampler, - sRGB); + CesiumGltf::Image& image = model.images[*optionalSourceIndex]; + const CesiumGltf::Sampler& sampler = + model.getSafe(model.samplers, texture.sampler); - if (result) { - extension.pTexture = result->pTexture; - result->textureIndex = textureIndex; - } + TUniquePtr result = + loadTextureFromImageAndSamplerAnyThreadPart(image.cesium, sampler, sRGB); - return result; + if (result) { + extension.pTexture = result->pTexture; + result->textureIndex = textureIndex; } - TextureFilter - getTextureFilterFromSampler(const CesiumGltf::Sampler& sampler) { - // Unreal Engine's available filtering modes are only nearest, bilinear, - // trilinear, and "default". Default means "use the texture group settings", - // and the texture group settings are defined in a config file and can - // vary per platform. All filter modes can use mipmaps if they're available, - // but only TF_Default will ever use anisotropic texture filtering. - // - // Unreal also doesn't separate the minification filter from the - // magnification filter. So we'll just ignore the magFilter unless it's the - // only filter specified. - // - // Generally our bias is toward TF_Default, because that gives the user more - // control via texture groups. - - if (sampler.magFilter && !sampler.minFilter) { - // Only a magnification filter is specified, so use it. - return sampler.magFilter.value() == - CesiumGltf::Sampler::MagFilter::NEAREST - ? TextureFilter::TF_Nearest - : TextureFilter::TF_Default; - } else if (sampler.minFilter) { - // Use specified minFilter. - switch (sampler.minFilter.value()) { - case CesiumGltf::Sampler::MinFilter::NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - return TextureFilter::TF_Nearest; - case CesiumGltf::Sampler::MinFilter::LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - return TextureFilter::TF_Bilinear; - default: - return TextureFilter::TF_Default; - } - } else { - // No filtering specified at all, let the texture group decide. + return result; +} + +TextureFilter getTextureFilterFromSampler(const CesiumGltf::Sampler& sampler) { + // Unreal Engine's available filtering modes are only nearest, bilinear, + // trilinear, and "default". Default means "use the texture group settings", + // and the texture group settings are defined in a config file and can + // vary per platform. All filter modes can use mipmaps if they're available, + // but only TF_Default will ever use anisotropic texture filtering. + // + // Unreal also doesn't separate the minification filter from the + // magnification filter. So we'll just ignore the magFilter unless it's the + // only filter specified. + // + // Generally our bias is toward TF_Default, because that gives the user more + // control via texture groups. + + if (sampler.magFilter && !sampler.minFilter) { + // Only a magnification filter is specified, so use it. + return sampler.magFilter.value() == CesiumGltf::Sampler::MagFilter::NEAREST + ? TextureFilter::TF_Nearest + : TextureFilter::TF_Default; + } else if (sampler.minFilter) { + // Use specified minFilter. + switch (sampler.minFilter.value()) { + case CesiumGltf::Sampler::MinFilter::NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return TextureFilter::TF_Nearest; + case CesiumGltf::Sampler::MinFilter::LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + return TextureFilter::TF_Bilinear; + default: return TextureFilter::TF_Default; } + } else { + // No filtering specified at all, let the texture group decide. + return TextureFilter::TF_Default; } +} - bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { - switch (sampler.minFilter.value_or( - CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - return true; - break; - default: // LINEAR and NEAREST - return false; - } +bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { + switch (sampler.minFilter.value_or( + CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: + case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: + return true; + break; + default: // LINEAR and NEAREST + return false; } +} + +TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( + CesiumGltf::SharedAsset& image, + const CesiumGltf::Sampler& sampler, + bool sRGB) { + return loadTextureAnyThreadPart( + image, + convertGltfWrapSToUnreal(sampler.wrapS), + convertGltfWrapTToUnreal(sampler.wrapT), + getTextureFilterFromSampler(sampler), + getUseMipmapsIfAvailableFromSampler(sampler), + // TODO: allow texture group to be configured on Cesium3DTileset. + TEXTUREGROUP_World, + sRGB, + std::nullopt); +} - TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::SharedAsset& image, - const CesiumGltf::Sampler& sampler, - bool sRGB) { - return loadTextureAnyThreadPart( - image, - convertGltfWrapSToUnreal(sampler.wrapS), - convertGltfWrapTToUnreal(sampler.wrapT), - getTextureFilterFromSampler(sampler), - getUseMipmapsIfAvailableFromSampler(sampler), - // TODO: allow texture group to be configured on Cesium3DTileset. - TEXTUREGROUP_World, - sRGB, - std::nullopt); +static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { + if (!pHalfLoadedTexture || !pHalfLoadedTexture->pTexture) { + return nullptr; } - static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { - if (!pHalfLoadedTexture || !pHalfLoadedTexture->pTexture) { - return nullptr; - } + UTexture2D* pTexture = pHalfLoadedTexture->pTexture->getUnrealTexture(); + if (!pTexture) { + pTexture = NewObject( + GetTransientPackage(), + MakeUniqueObjectName( + GetTransientPackage(), + UTexture2D::StaticClass(), + "CesiumRuntimeTexture"), + RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); + + pTexture->AddressX = pHalfLoadedTexture->addressX; + pTexture->AddressY = pHalfLoadedTexture->addressY; + pTexture->Filter = pHalfLoadedTexture->filter; + pTexture->LODGroup = pHalfLoadedTexture->group; + pTexture->SRGB = pHalfLoadedTexture->sRGB; + + pTexture->NeverStream = true; + + pHalfLoadedTexture->pTexture->setUnrealTexture(pTexture); + } - UTexture2D* pTexture = pHalfLoadedTexture->pTexture->getUnrealTexture(); - if (!pTexture) { - pTexture = NewObject( - GetTransientPackage(), - MakeUniqueObjectName( - GetTransientPackage(), - UTexture2D::StaticClass(), - "CesiumRuntimeTexture"), - RF_Transient | RF_DuplicateTransient | RF_TextExportTransient); - - pTexture->AddressX = pHalfLoadedTexture->addressX; - pTexture->AddressY = pHalfLoadedTexture->addressY; - pTexture->Filter = pHalfLoadedTexture->filter; - pTexture->LODGroup = pHalfLoadedTexture->group; - pTexture->SRGB = pHalfLoadedTexture->sRGB; - - pTexture->NeverStream = true; - - pHalfLoadedTexture->pTexture->setUnrealTexture(pTexture); - } + return pTexture; +} - return pTexture; +TUniquePtr loadTextureAnyThreadPart( + CesiumGltf::SharedAsset& image, + TextureAddress addressX, + TextureAddress addressY, + TextureFilter filter, + bool useMipMapsIfAvailable, + TextureGroup group, + bool sRGB, + std::optional overridePixelFormat) { + // The FCesiumTextureResource for the ImageCesium should already be created at + // this point, if it can be. + ExtensionImageCesiumUnreal* pExtension = + image->getExtension(); + if (pExtension == nullptr || pExtension->getTextureResource() == nullptr) { + return nullptr; } - TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::SharedAsset& image, - TextureAddress addressX, - TextureAddress addressY, - TextureFilter filter, - bool useMipMapsIfAvailable, - TextureGroup group, - bool sRGB, - std::optional overridePixelFormat) { - TSharedPtr textureResource = - ExtensionUnrealTextureResource::loadTextureResource( - *image, - addressX, - addressY, - filter, - useMipMapsIfAvailable, - group, - sRGB, - overridePixelFormat); + auto pResource = FCesiumTextureResource::CreateWrapped( + pExtension->getTextureResource(), + group, + filter, + addressX, + addressY, + sRGB, + useMipMapsIfAvailable); + + TUniquePtr pResult = MakeUnique(); + pResult->pTexture = new ReferenceCountedUnrealTexture(); + + pResult->addressX = addressX; + pResult->addressY = addressY; + pResult->filter = filter; + pResult->group = group; + pResult->sRGB = sRGB; + pResult->pTexture->setTextureResource(MoveTemp(pResource)); + + return pResult; +} - TUniquePtr pResult = MakeUnique(); - pResult->pTexture = new ReferenceCountedUnrealTexture(); +CesiumUtility::IntrusivePointer +loadTextureGameThreadPart( + CesiumGltf::Model& model, + LoadedTextureResult* pHalfLoadedTexture) { + if (pHalfLoadedTexture == nullptr) + return nullptr; - pResult->addressX = addressX; - pResult->addressY = addressY; - pResult->filter = filter; - pResult->group = group; - pResult->sRGB = sRGB; + CesiumUtility::IntrusivePointer pResult = + loadTextureGameThreadPart(pHalfLoadedTexture); - pResult->pTexture->setTextureResource(MoveTemp(textureResource)); - pResult->pTexture->setSharedImage(image); - return pResult; + if (pResult && pHalfLoadedTexture && pHalfLoadedTexture->textureIndex >= 0 && + size_t(pHalfLoadedTexture->textureIndex) < model.textures.size()) { + CesiumGltf::Texture& texture = + model.textures[pHalfLoadedTexture->textureIndex]; + ExtensionUnrealTexture& extension = + texture.addExtension(); + extension.pTexture = pHalfLoadedTexture->pTexture; } - CesiumUtility::IntrusivePointer - loadTextureGameThreadPart( - CesiumGltf::Model& model, - LoadedTextureResult* pHalfLoadedTexture) { - if (pHalfLoadedTexture == nullptr) - return nullptr; - - CesiumUtility::IntrusivePointer pResult = - loadTextureGameThreadPart(pHalfLoadedTexture); - - if (pResult && pHalfLoadedTexture && - pHalfLoadedTexture->textureIndex >= 0 && - size_t(pHalfLoadedTexture->textureIndex) < model.textures.size()) { - CesiumGltf::Texture& texture = - model.textures[pHalfLoadedTexture->textureIndex]; - ExtensionUnrealTexture& extension = - texture.addExtension(); - extension.pTexture = pHalfLoadedTexture->pTexture; - } + return pHalfLoadedTexture->pTexture; +} +CesiumUtility::IntrusivePointer +loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { + TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTexture) + + FCesiumTextureResourceUniquePtr& pTextureResource = + pHalfLoadedTexture->pTexture->getTextureResource(); + if (pTextureResource == nullptr) { + // Texture is already loaded (or unloadable). return pHalfLoadedTexture->pTexture; } - CesiumUtility::IntrusivePointer - loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { - TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::LoadTexture) - - TSharedPtr& pTextureResource = - pHalfLoadedTexture->pTexture->getTextureResource(); - if (pTextureResource == nullptr) { - // Texture is already loaded (or unloadable). - return pHalfLoadedTexture->pTexture; - } - - UTexture2D* pTexture = CreateTexture2D(pHalfLoadedTexture); - if (pTexture == nullptr) { - return nullptr; - } + UTexture2D* pTexture = CreateTexture2D(pHalfLoadedTexture); + if (pTexture == nullptr) { + return nullptr; + } - FCesiumTextureResourceBase* pCesiumTextureResource = - pTextureResource.Release(); - if (pCesiumTextureResource) { - pTexture->SetResource(pCesiumTextureResource); + if (pTextureResource) { + // Give the UTexture2D exclusive ownership of this FCesiumTextureResource. + pTexture->SetResource(pTextureResource.Release()); - ENQUEUE_RENDER_COMMAND(Cesium_InitResource) - ([pTexture, - pCesiumTextureResource](FRHICommandListImmediate& RHICmdList) { - pCesiumTextureResource->SetTextureReference( - pTexture->TextureReference.TextureReferenceRHI); + ENQUEUE_RENDER_COMMAND(Cesium_InitResource) + ([pTexture, pTextureResource = pTexture->GetResource()]( + FRHICommandListImmediate& RHICmdList) { + pTextureResource->SetTextureReference( + pTexture->TextureReference.TextureReferenceRHI); #if ENGINE_VERSION_5_3_OR_HIGHER - pCesiumTextureResource->InitResource( - FRHICommandListImmediate::Get()); // Init Resource now requires a - // command list. + pCesiumTextureResource->InitResource( + FRHICommandListImmediate::Get()); // Init Resource now requires a + // command list. #else - pCesiumTextureResource->InitResource(); + pTextureResource->InitResource(); #endif - }); - } - - return pHalfLoadedTexture->pTexture; + }); } - TSharedPtr createTextureResource( - CesiumGltf::ImageCesium& imageCesium, - bool sRGB, - std::optional overridePixelFormat) { - std::optional optionalPixelFormat = - getPixelFormatForImageCesium(imageCesium, overridePixelFormat); - if (!optionalPixelFormat.has_value()) { - return nullptr; - } + return pHalfLoadedTexture->pTexture; +} - EPixelFormat pixelFormat = optionalPixelFormat.value(); - - // Store the current size of the pixel data, because - // we're about to clear it but we still want to have - // an accurate estimation of the size of the image for - // caching purposes. - imageCesium.sizeBytes = int64_t(imageCesium.pixelData.size()); - - return createTextureResourceFromImageCesium( - imageCesium, - TextureAddress::TA_Wrap, - TextureAddress::TA_Wrap, - TextureFilter::TF_Default, - true, - TextureGroup::TEXTUREGROUP_World, - sRGB, - pixelFormat); - } - - TextureAddress convertGltfWrapSToUnreal(int32_t wrapS) { - // glTF spec: "When undefined, a sampler with repeat wrapping and auto - // filtering should be used." - switch (wrapS) { - case CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE: - return TextureAddress::TA_Clamp; - case CesiumGltf::Sampler::WrapS::MIRRORED_REPEAT: - return TextureAddress::TA_Mirror; - case CesiumGltf::Sampler::WrapS::REPEAT: - default: - return TextureAddress::TA_Wrap; - } +TextureAddress convertGltfWrapSToUnreal(int32_t wrapS) { + // glTF spec: "When undefined, a sampler with repeat wrapping and auto + // filtering should be used." + switch (wrapS) { + case CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE: + return TextureAddress::TA_Clamp; + case CesiumGltf::Sampler::WrapS::MIRRORED_REPEAT: + return TextureAddress::TA_Mirror; + case CesiumGltf::Sampler::WrapS::REPEAT: + default: + return TextureAddress::TA_Wrap; } +} - TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { - // glTF spec: "When undefined, a sampler with repeat wrapping and auto - // filtering should be used." - switch (wrapT) { - case CesiumGltf::Sampler::WrapT::CLAMP_TO_EDGE: - return TextureAddress::TA_Clamp; - case CesiumGltf::Sampler::WrapT::MIRRORED_REPEAT: - return TextureAddress::TA_Mirror; - case CesiumGltf::Sampler::WrapT::REPEAT: - default: - return TextureAddress::TA_Wrap; - } +TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { + // glTF spec: "When undefined, a sampler with repeat wrapping and auto + // filtering should be used." + switch (wrapT) { + case CesiumGltf::Sampler::WrapT::CLAMP_TO_EDGE: + return TextureAddress::TA_Clamp; + case CesiumGltf::Sampler::WrapT::MIRRORED_REPEAT: + return TextureAddress::TA_Mirror; + case CesiumGltf::Sampler::WrapT::REPEAT: + default: + return TextureAddress::TA_Wrap; } +} - std::optional> - createMipMapsForSampler( - const CesiumAsync::AsyncSystem& asyncSystem, - const CesiumGltf::Sampler& sampler, - CesiumGltf::ImageCesium& image) { - std::unique_lock lock(textureResourceMutex); - - ExtensionUnrealTextureResource& extension = - image.addExtension(); - - // Future already exists, we don't need to do anything else. - if (extension.preprocessFuture.has_value()) { - return extension.preprocessFuture.value(); - } - - // Generate mipmaps if needed. - // An image needs mipmaps generated for it if: - // 1. It is used by a Texture that has a Sampler with a mipmap filtering - // mode, and - // 2. It does not already have mipmaps. - // It's ok if an image has mipmaps even if not all textures will use them. - // There's no reason to have two RHI textures, one with and one without - // mips. - bool needsMipmaps; - switch (sampler.minFilter.value_or( - CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR)) { - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::LINEAR_MIPMAP_NEAREST: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_LINEAR: - case CesiumGltf::Sampler::MinFilter::NEAREST_MIPMAP_NEAREST: - needsMipmaps = true; +std::optional getPixelFormatForImageCesium( + const ImageCesium& imageCesium, + const std::optional overridePixelFormat) { + if (imageCesium.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { + switch (imageCesium.compressedPixelFormat) { + case GpuCompressedPixelFormat::ETC1_RGB: + return EPixelFormat::PF_ETC1; break; - default: // LINEAR and NEAREST - needsMipmaps = false; + case GpuCompressedPixelFormat::ETC2_RGBA: + return EPixelFormat::PF_ETC2_RGBA; break; - } - - if (!needsMipmaps || image.pixelData.empty()) { - // If we don't need mipmaps, we don't want to create a future for them. - // This allows a future sampler using this image that does need mipmaps to - // generate them. + case GpuCompressedPixelFormat::BC1_RGB: + return EPixelFormat::PF_DXT1; + break; + case GpuCompressedPixelFormat::BC3_RGBA: + return EPixelFormat::PF_DXT5; + break; + case GpuCompressedPixelFormat::BC4_R: + return EPixelFormat::PF_BC4; + break; + case GpuCompressedPixelFormat::BC5_RG: + return EPixelFormat::PF_BC5; + break; + case GpuCompressedPixelFormat::BC7_RGBA: + return EPixelFormat::PF_BC7; + break; + case GpuCompressedPixelFormat::ASTC_4x4_RGBA: + return EPixelFormat::PF_ASTC_4x4; + break; + case GpuCompressedPixelFormat::PVRTC2_4_RGBA: + return EPixelFormat::PF_PVRTC2; + break; + case GpuCompressedPixelFormat::ETC2_EAC_R11: + return EPixelFormat::PF_ETC2_R11_EAC; + break; + case GpuCompressedPixelFormat::ETC2_EAC_RG11: + return EPixelFormat::PF_ETC2_RG11_EAC; + break; + default: + // Unsupported compressed texture format. return std::nullopt; - } - - CesiumAsync::Promise promise = - asyncSystem.createPromise(); - - extension.preprocessFuture = promise.getFuture().share(); - - lock.unlock(); - - // We need mipmaps generated. - std::optional errorMessage = - CesiumGltfReader::GltfReader::generateMipMaps(image); - if (errorMessage) { - UE_LOG( - LogCesium, - Warning, - TEXT("%s"), - UTF8_TO_TCHAR(errorMessage->c_str())); - } - promise.resolve(&image); - return *extension.preprocessFuture; + }; + } else if (overridePixelFormat) { + return *overridePixelFormat; + } else { + switch (imageCesium.channels) { + case 1: + return PF_R8; + break; + case 2: + return PF_R8G8; + break; + case 3: + case 4: + default: + return PF_R8G8B8A8; + }; } - CesiumAsync::SharedFuture createMipMapsForAllTextures( - const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::Model& model) { - std::vector> futures; - for (const Texture& texture : model.textures) { - const CesiumGltf::Sampler& sampler = - model.getSafe(model.samplers, texture.sampler); - std::optional> - optionalFuture = createMipMapsForSampler( - asyncSystem, - sampler, - *model.images[texture.source].cesium); - if (optionalFuture.has_value()) { - futures.push_back(optionalFuture.value()); - } - } - - return asyncSystem.all(std::move(futures)) - .thenImmediately( - []([[maybe_unused]] std::vector< - CesiumGltf::ImageCesium*>&& /*results*/) -> void {}) - .share(); - } + return std::nullopt; +} } // namespace CesiumTextureUtility diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 767b6da4e..e037c4e95 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -47,18 +47,13 @@ struct ReferenceCountedUnrealTexture void setUnrealTexture(const TObjectPtr& p); // The renderer / RHI FTextureResource holding the pixel data. - const TSharedPtr& getTextureResource() const; - TSharedPtr& getTextureResource(); - void setTextureResource(TSharedPtr&& p); - - /// The SharedAsset that this texture was created from. - CesiumGltf::SharedAsset getSharedImage() const; - void setSharedImage(CesiumGltf::SharedAsset& image); + const FCesiumTextureResourceUniquePtr& getTextureResource() const; + FCesiumTextureResourceUniquePtr& getTextureResource(); + void setTextureResource(FCesiumTextureResourceUniquePtr&& p); private: TObjectPtr _pUnrealTexture; - TSharedPtr _pTextureResource; - CesiumGltf::SharedAsset _pImageCesium; + FCesiumTextureResourceUniquePtr _pTextureResource; }; /** @@ -191,7 +186,7 @@ loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture); * intended to be later used with `FCesiumUseExistingTextureResource`, which * will supply sampler, texture group, and other settings. */ -TSharedPtr createTextureResource( +TSharedPtr createTextureResource( CesiumGltf::ImageCesium& imageCesium, bool sRGB, std::optional overridePixelFormat); @@ -216,8 +211,8 @@ TextureAddress convertGltfWrapSToUnreal(int32_t wrapS); */ TextureAddress convertGltfWrapTToUnreal(int32_t wrapT); -CesiumAsync::SharedFuture createMipMapsForAllTextures( - const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::Model& model); +std::optional getPixelFormatForImageCesium( + const CesiumGltf::ImageCesium& imageCesium, + const std::optional overridePixelFormat); } // namespace CesiumTextureUtility diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp new file mode 100644 index 000000000..e77409a24 --- /dev/null +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp @@ -0,0 +1,101 @@ +#include "ExtensionImageCesiumUnreal.h" +#include "CesiumRuntime.h" +#include "CesiumTextureUtility.h" +#include +#include + +using namespace CesiumAsync; +using namespace CesiumGltf; +using namespace CesiumGltfReader; + +namespace { + +std::mutex createExtensionMutex; + +std::pair>> +getOrCreateImageFuture( + const AsyncSystem& asyncSystem, + ImageCesium& imageCesium); + +} // namespace + +/*static*/ const ExtensionImageCesiumUnreal& +ExtensionImageCesiumUnreal::GetOrCreate( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::ImageCesium& imageCesium, + bool needsMipMaps, + const std::optional& overridePixelFormat) { + auto [extension, maybePromise] = + getOrCreateImageFuture(asyncSystem, imageCesium); + if (!maybePromise) { + // Another thread is already working on this image. + return extension; + } + + // Proceed to load the image in this thread. + TUniquePtr pResource = + FCesiumTextureResource::CreateNew( + imageCesium, + TextureGroup::TEXTUREGROUP_World, + overridePixelFormat, + TextureFilter::TF_Default, + TextureAddress::TA_Clamp, + TextureAddress::TA_Clamp, + false, + needsMipMaps); + + extension._pTextureResource = + MakeShareable(pResource.Release(), [](FCesiumTextureResource* p) { + FCesiumTextureResource ::Destroy(p); + }); + + maybePromise->resolve(); + + return extension; +} + +ExtensionImageCesiumUnreal::ExtensionImageCesiumUnreal( + const CesiumAsync::SharedFuture& future) + : _pTextureResource(nullptr), _futureCreateResource(future) {} + +const TSharedPtr& +ExtensionImageCesiumUnreal::getTextureResource() const { + return this->_pTextureResource; +} + +CesiumAsync::SharedFuture& ExtensionImageCesiumUnreal::getFuture() { + return this->_futureCreateResource; +} + +const CesiumAsync::SharedFuture& +ExtensionImageCesiumUnreal::getFuture() const { + return this->_futureCreateResource; +} + +namespace { + +// Returns a Future that will resolve when the image is loaded. It _may_ also +// return a Promise, in which case the calling thread is responsible for doing +// the loading and should resolve the Promise when it's done. +std::pair>> +getOrCreateImageFuture( + const AsyncSystem& asyncSystem, + ImageCesium& imageCesium) { + std::scoped_lock lock(createExtensionMutex); + + ExtensionImageCesiumUnreal* pExtension = + imageCesium.getExtension(); + if (!pExtension) { + // This thread will work on this image. + Promise promise = asyncSystem.createPromise(); + ExtensionImageCesiumUnreal& extension = + imageCesium.addExtension( + promise.getFuture().share()); + return {extension, std::move(promise)}; + } else { + // Another thread is already working on this image. + return {*pExtension, std::nullopt}; + } +} + +} // namespace diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h new file mode 100644 index 000000000..84b7618a6 --- /dev/null +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h @@ -0,0 +1,68 @@ +// Copyright 2020-2024 CesiumGS, Inc. and Contributors + +#pragma once + +#include "CesiumTextureResource.h" +#include "PixelFormat.h" +#include "Templates/SharedPointer.h" +#include +#include + +namespace CesiumGltf { +struct ImageCesium; +} + +/** + * @brief An extension attached to an ImageCesium in order to hold + * Unreal-specific information about it. + * + * ImageCesium instances are shared between multiple textures on a single model, + * and even between models in some cases, but we strive to have only one copy of + * the image bytes in GPU memory. + * + * The Unreal / GPU resource is held in `pTextureResource`, which may be either + * a `FCesiumCreateNewTextureResource` or a `FCesiumUseExistingTextureResource` + * depending on how it was created. We'll never actually sample directly from + * this resource, however. Instead, a separate + * `FCesiumUseExistingTextureResource` will be created for each glTF Texture + * that references this image, and it will point to the instance managed by this + * extension. + * + * Because we'll never be sampling from this texture resource, the texture + * filtering and addressing parameters have default values. + */ +struct ExtensionImageCesiumUnreal { + static inline constexpr const char* TypeName = "ExtensionImageCesiumUnreal"; + static inline constexpr const char* ExtensionName = + "PRIVATE_ImageCesium_Unreal"; + + /** + * @brief Gets an Unreal texture resource from the given `ImageCesium`, + * creating it if necessary. + * + * When this function is called for the first time on a particular + * `ImageCesium`, the asynchronous process to create an Unreal + * `FTextureResource` from it is kicked off. On successive invocations + * (perhaps from other threads), the existing instance is returned. It is safe + * to call this method on the same `ImageCesium` instance from multiple + * threads simultaneously. + * + * To determine if the asynchronous `FTextureResource` creation process has + * completed, use {@link getFuture}. + */ + static const ExtensionImageCesiumUnreal& GetOrCreate( + const CesiumAsync::AsyncSystem& asyncSystem, + CesiumGltf::ImageCesium& imageCesium, + bool needsMipMaps, + const std::optional& overridePixelFormat); + + ExtensionImageCesiumUnreal(const CesiumAsync::SharedFuture& future); + + const TSharedPtr& getTextureResource() const; + CesiumAsync::SharedFuture& getFuture(); + const CesiumAsync::SharedFuture& getFuture() const; + +private: + TSharedPtr _pTextureResource; + CesiumAsync::SharedFuture _futureCreateResource; +}; diff --git a/extern/cesium-native b/extern/cesium-native index 6de6b3860..86995ffd4 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 6de6b386035be9cee3e4ede5c50391acd27f921e +Subproject commit 86995ffd49c33774ea3a7b6f3613cfeebd402e2e From ceca50472d56c6d66d9f442f786c5ef46ddb6689 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 14:57:30 +1000 Subject: [PATCH 03/23] Undo accidental rename. --- Source/CesiumRuntime/Private/CesiumGltfComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index e8d2c5620..cc535c220 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -1062,7 +1062,7 @@ std::string constrainLength(const std::string& s, const size_t maxLength) { } /** - * @brief CreateNew an FName from the given strings. + * @brief Create an FName from the given strings. * * This will combine the prefix and the suffix and create an FName. * If the string would be longer than the given length, then From 306728406c5928ecd3b9ece0d4d7ea1fc8bcf7b2 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 15:47:22 +1000 Subject: [PATCH 04/23] Use correct sRGB setting when initially creating texture. --- Source/CesiumRuntime/Private/CesiumGltfTextures.cpp | 12 +++++++----- .../Private/ExtensionImageCesiumUnreal.cpp | 3 ++- .../Private/ExtensionImageCesiumUnreal.h | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index 95cfe003c..d7147b797 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -201,11 +201,13 @@ SharedFuture createTextureInLoadThread( check(pTexture->source >= 0 && pTexture->source < imageNeedsMipmaps.size()); bool needsMips = imageNeedsMipmaps[pTexture->source]; - const ExtensionImageCesiumUnreal& extension = ExtensionImageCesiumUnreal::GetOrCreate( - asyncSystem, - *pImage->cesium, - needsMips, - std::nullopt); + const ExtensionImageCesiumUnreal& extension = + ExtensionImageCesiumUnreal::GetOrCreate( + asyncSystem, + *pImage->cesium, + sRGB, + needsMips, + std::nullopt); return extension.getFuture(); } diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp index e77409a24..f24698f52 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp @@ -23,6 +23,7 @@ getOrCreateImageFuture( ExtensionImageCesiumUnreal::GetOrCreate( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::ImageCesium& imageCesium, + bool sRGB, bool needsMipMaps, const std::optional& overridePixelFormat) { auto [extension, maybePromise] = @@ -41,7 +42,7 @@ ExtensionImageCesiumUnreal::GetOrCreate( TextureFilter::TF_Default, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, - false, + sRGB, needsMipMaps); extension._pTextureResource = diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h index 84b7618a6..d01ece599 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h @@ -53,6 +53,7 @@ struct ExtensionImageCesiumUnreal { static const ExtensionImageCesiumUnreal& GetOrCreate( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::ImageCesium& imageCesium, + bool sRGB, bool needsMipMaps, const std::optional& overridePixelFormat); From aa58bd932bdd8acb2c2ff854a2fe5bcfcf77a7b8 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 16:28:20 +1000 Subject: [PATCH 05/23] Fix UE 5.3/5.4 compile error. --- Source/CesiumRuntime/Private/CesiumTextureResource.cpp | 2 +- Source/CesiumRuntime/Private/CesiumTextureUtility.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index b3ca34a8b..7fa9cd680 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp @@ -461,7 +461,7 @@ FCesiumTextureResource::FCesiumTextureResource( } #if ENGINE_VERSION_5_3_OR_HIGHER -void FCesiumTextureResourceBase::InitRHI(FRHICommandListBase& RHICmdList) { +void FCesiumTextureResource::InitRHI(FRHICommandListBase& RHICmdList) { #else void FCesiumTextureResource::InitRHI() { #endif diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index e037c4e95..c37fd3e05 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -182,7 +182,7 @@ CesiumUtility::IntrusivePointer loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture); /** - * Creates a `FCesiumTextureResourceBase` for an image. This texture resource is + * Creates a `FCesiumTextureResource` for an image. This texture resource is * intended to be later used with `FCesiumUseExistingTextureResource`, which * will supply sampler, texture group, and other settings. */ From 1246c2d8f787ddd76386b639ee160c5578f2ac03 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 17:13:05 +1000 Subject: [PATCH 06/23] Fix raster overlays and water mask. --- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 23 ++++++++++--- .../Private/CesiumEncodedFeaturesMetadata.cpp | 6 ++-- .../Private/CesiumEncodedMetadataUtility.cpp | 6 ++-- .../Private/CesiumGltfTextures.cpp | 32 +++++++++++++++++++ .../Private/CesiumTextureUtility.cpp | 11 ++++--- .../Private/CesiumTextureUtility.h | 28 ++++------------ .../Tests/CesiumTextureUtility.spec.cpp | 6 ++-- 7 files changed, 72 insertions(+), 40 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index af8a85824..2cd14ad49 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -41,6 +41,7 @@ #include "Engine/TextureRenderTarget2D.h" #include "Engine/World.h" #include "EngineUtils.h" +#include "ExtensionImageCesiumUnreal.h" #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" #include "LevelSequenceActor.h" @@ -877,19 +878,31 @@ class UnrealResourcePreparer } } - CesiumGltf::SharedAsset imageAsset( - std::move(image)); + // TODO: sRGB should probably be configurable on the raster overlay. + bool sRGB = true; + + const ExtensionImageCesiumUnreal& extension = + ExtensionImageCesiumUnreal::GetOrCreate( + CesiumAsync::AsyncSystem(nullptr), // TODO + image, + sRGB, + pOptions->useMipmaps, + std::nullopt); + + // Because raster overlay images are never shared (at least currently!), the + // future should already be resolved by the time we get here. + check(extension.getFuture().isReady()); auto texture = CesiumTextureUtility::loadTextureAnyThreadPart( - imageAsset, + image, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, pOptions->filter, pOptions->useMipmaps, pOptions->group, - // TODO: sRGB should probably be configurable on the raster overlay. - true, + sRGB, std::nullopt); + return texture.Release(); } diff --git a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp index c4b9963b2..475729dc8 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp +++ b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp @@ -147,7 +147,7 @@ std::optional encodeFeatureIdTexture( CesiumGltf::SharedAsset(CesiumGltf::ImageCesium(*pFeatureIdImage)); encodedFeatureIdTexture.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( - imageCopy, + *imageCopy, addressX, addressY, TextureFilter::TF_Nearest, @@ -534,7 +534,7 @@ EncodedPropertyTable encodePropertyTableAnyThreadPart( } encodedProperty.pTexture = loadTextureAnyThreadPart( - image, + *image, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, @@ -661,7 +661,7 @@ EncodedPropertyTexture encodePropertyTextureAnyThreadPart( CesiumGltf::SharedAsset(CesiumGltf::ImageCesium(*pImage)); encodedProperty.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( - imageCopy, + *imageCopy, addressX, addressY, // TODO: account for texture filter diff --git a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp index 7c36b7e70..bc54d4249 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp @@ -278,7 +278,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( } encodedProperty.pTexture = loadTextureAnyThreadPart( - image, + *image, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, @@ -414,7 +414,7 @@ EncodedFeatureTexture encodeFeatureTextureAnyThreadPart( CesiumGltf::SharedAsset(CesiumGltf::ImageCesium(*pImage)); encodedFeatureTextureProperty.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( - imageCopy, + *imageCopy, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, @@ -521,7 +521,7 @@ EncodedMetadataPrimitive encodeMetadataPrimitiveAnyThreadPart( CesiumGltf::ImageCesium(*pFeatureIdImage)); encodedFeatureIdTexture.pTexture = MakeShared( std::move(*loadTextureAnyThreadPart( - imageCopy, + *imageCopy, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index d7147b797..0f022414e 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -116,6 +116,38 @@ SharedFuture createTextureInLoadThread( *pMaterial->occlusionTexture, false, imageNeedsMipmaps)); + + // Initialize water mask if needed. + auto onlyWaterIt = primitive.extras.find("OnlyWater"); + auto onlyLandIt = primitive.extras.find("OnlyLand"); + if (onlyWaterIt != primitive.extras.end() && + onlyWaterIt->second.isBool() && + onlyLandIt != primitive.extras.end() && + onlyLandIt->second.isBool()) { + bool onlyWater = onlyWaterIt->second.getBoolOrDefault(false); + bool onlyLand = onlyLandIt->second.getBoolOrDefault(true); + + if (!onlyWater && !onlyLand) { + // We have to use the water mask + auto waterMaskTextureIdIt = primitive.extras.find("WaterMaskTex"); + if (waterMaskTextureIdIt != primitive.extras.end() && + waterMaskTextureIdIt->second.isInt64()) { + int32_t waterMaskTextureId = static_cast( + waterMaskTextureIdIt->second.getInt64OrDefault(-1)); + TextureInfo waterMaskInfo; + waterMaskInfo.index = waterMaskTextureId; + if (waterMaskTextureId >= 0 && + waterMaskTextureId < gltf.textures.size()) { + futures.emplace_back(createTextureInLoadThread( + asyncSystem, + gltf, + waterMaskInfo, + false, + imageNeedsMipmaps)); + } + } + } + } }); return asyncSystem.all(std::move(futures)); diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index edad7fd09..81139f54e 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -304,7 +304,7 @@ TUniquePtr loadTextureFromModelAnyThreadPart( model.getSafe(model.samplers, texture.sampler); TUniquePtr result = - loadTextureFromImageAndSamplerAnyThreadPart(image.cesium, sampler, sRGB); + loadTextureFromImageAndSamplerAnyThreadPart(*image.cesium, sampler, sRGB); if (result) { extension.pTexture = result->pTexture; @@ -366,7 +366,7 @@ bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { } TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::SharedAsset& image, + const CesiumGltf::ImageCesium& image, const CesiumGltf::Sampler& sampler, bool sRGB) { return loadTextureAnyThreadPart( @@ -411,7 +411,7 @@ static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { } TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::SharedAsset& image, + const CesiumGltf::ImageCesium& image, TextureAddress addressX, TextureAddress addressY, TextureFilter filter, @@ -421,8 +421,9 @@ TUniquePtr loadTextureAnyThreadPart( std::optional overridePixelFormat) { // The FCesiumTextureResource for the ImageCesium should already be created at // this point, if it can be. - ExtensionImageCesiumUnreal* pExtension = - image->getExtension(); + const ExtensionImageCesiumUnreal* pExtension = + image.getExtension(); + check(pExtension != nullptr); if (pExtension == nullptr || pExtension->getTextureResource() == nullptr) { return nullptr; } diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index c37fd3e05..ff250ac44 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -117,16 +117,16 @@ TUniquePtr loadTextureFromModelAnyThreadPart( * and can be empty. */ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::SharedAsset& image, + const CesiumGltf::ImageCesium& image, const CesiumGltf::Sampler& sampler, bool sRGB); /** * @brief Does the asynchronous part of renderer resource preparation for - * this image. Should be called in a background thread. - * - * The `pixelData` will be removed from the image so that it can be - * passed to Unreal's renderer thread without copying it. + * a texture. The given image _must_ be prepared before calling this method by + * calling {@link ExtensionImageCesiumUnreal::GetOrCreate} and then waiting + * for {@link ExtensionImageCesiumUnreal::getFuture} to resolve. This method + * should be called in a background thread. * * @param imageCesium The image. * @param addressX The X addressing mode. @@ -138,14 +138,10 @@ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( * @param sRGB Whether this texture uses a sRGB color space. * @param overridePixelFormat The explicit pixel format to use. If std::nullopt, * the pixel format is inferred from the image. - * @param pExistingImageResource An existing RHI texture resource that has been - * created for this image, or nullptr if one hasn't been created yet. When this - * parameter is not nullptr, the provided image's `pixelData` is not required - * and can be empty. - * @return A future that resolves to the loaded texture. + * @return The loaded texture. */ TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::SharedAsset& image, + const CesiumGltf::ImageCesium& image, TextureAddress addressX, TextureAddress addressY, TextureFilter filter, @@ -181,16 +177,6 @@ loadTextureGameThreadPart( CesiumUtility::IntrusivePointer loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture); -/** - * Creates a `FCesiumTextureResource` for an image. This texture resource is - * intended to be later used with `FCesiumUseExistingTextureResource`, which - * will supply sampler, texture group, and other settings. - */ -TSharedPtr createTextureResource( - CesiumGltf::ImageCesium& imageCesium, - bool sRGB, - std::optional overridePixelFormat); - /** * @brief Convert a glTF {@link CesiumGltf::Sampler::WrapS} value to an Unreal * `TextureAddress` value. diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp index 515bdc0fe..0e9cac98f 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp @@ -105,7 +105,7 @@ void CesiumTextureUtilitySpec::Define() { void CesiumTextureUtilitySpec::RunTests() { It("ImageCesium non-sRGB", [this]() { TUniquePtr pHalfLoaded = loadTextureAnyThreadPart( - imageCesium, + *imageCesium, TextureAddress::TA_Mirror, TextureAddress::TA_Wrap, TextureFilter::TF_Bilinear, @@ -129,7 +129,7 @@ void CesiumTextureUtilitySpec::RunTests() { It("ImageCesium sRGB", [this]() { TUniquePtr pHalfLoaded = loadTextureAnyThreadPart( - imageCesium, + *imageCesium, TextureAddress::TA_Clamp, TextureAddress::TA_Mirror, TextureFilter::TF_Trilinear, @@ -160,7 +160,7 @@ void CesiumTextureUtilitySpec::RunTests() { TUniquePtr pHalfLoaded = loadTextureFromImageAndSamplerAnyThreadPart( - imageCesium, + *imageCesium, sampler, false); TestNotNull("pHalfLoaded", pHalfLoaded.Get()); From b75d27441c478b0c68be4e40159f69510102b3bb Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 18:11:17 +1000 Subject: [PATCH 07/23] GetOrCreate -> getOrCreate. --- Source/CesiumRuntime/Private/Cesium3DTileset.cpp | 2 +- Source/CesiumRuntime/Private/CesiumGltfTextures.cpp | 2 +- Source/CesiumRuntime/Private/CesiumTextureUtility.h | 2 +- Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp | 2 +- Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 2cd14ad49..3e2dcc9ef 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -882,7 +882,7 @@ class UnrealResourcePreparer bool sRGB = true; const ExtensionImageCesiumUnreal& extension = - ExtensionImageCesiumUnreal::GetOrCreate( + ExtensionImageCesiumUnreal::getOrCreate( CesiumAsync::AsyncSystem(nullptr), // TODO image, sRGB, diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index 0f022414e..a9c8b20fd 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -234,7 +234,7 @@ SharedFuture createTextureInLoadThread( bool needsMips = imageNeedsMipmaps[pTexture->source]; const ExtensionImageCesiumUnreal& extension = - ExtensionImageCesiumUnreal::GetOrCreate( + ExtensionImageCesiumUnreal::getOrCreate( asyncSystem, *pImage->cesium, sRGB, diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index ff250ac44..4a9b3bbe7 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -124,7 +124,7 @@ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( /** * @brief Does the asynchronous part of renderer resource preparation for * a texture. The given image _must_ be prepared before calling this method by - * calling {@link ExtensionImageCesiumUnreal::GetOrCreate} and then waiting + * calling {@link ExtensionImageCesiumUnreal::getOrCreate} and then waiting * for {@link ExtensionImageCesiumUnreal::getFuture} to resolve. This method * should be called in a background thread. * diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp index f24698f52..bb0df6f8d 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp @@ -20,7 +20,7 @@ getOrCreateImageFuture( } // namespace /*static*/ const ExtensionImageCesiumUnreal& -ExtensionImageCesiumUnreal::GetOrCreate( +ExtensionImageCesiumUnreal::getOrCreate( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::ImageCesium& imageCesium, bool sRGB, diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h index d01ece599..88ea0b16a 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h @@ -50,7 +50,7 @@ struct ExtensionImageCesiumUnreal { * To determine if the asynchronous `FTextureResource` creation process has * completed, use {@link getFuture}. */ - static const ExtensionImageCesiumUnreal& GetOrCreate( + static const ExtensionImageCesiumUnreal& getOrCreate( const CesiumAsync::AsyncSystem& asyncSystem, CesiumGltf::ImageCesium& imageCesium, bool sRGB, From 8141c5ca48e61f3762b83037ae98e4db08de6b6f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 18:13:40 +1000 Subject: [PATCH 08/23] Fix another UE 5.3/5.4 compile error. --- Source/CesiumRuntime/Private/CesiumTextureUtility.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index 81139f54e..2e47c46b7 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -498,7 +498,7 @@ loadTextureGameThreadPart(LoadedTextureResult* pHalfLoadedTexture) { pTextureResource->SetTextureReference( pTexture->TextureReference.TextureReferenceRHI); #if ENGINE_VERSION_5_3_OR_HIGHER - pCesiumTextureResource->InitResource( + pTextureResource->InitResource( FRHICommandListImmediate::Get()); // Init Resource now requires a // command list. #else From e4734e36f23346ae7dc51d21bf88e2870c0424de Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 20:53:31 +1000 Subject: [PATCH 09/23] Fix test failures. --- .../Private/CesiumTextureUtility.cpp | 19 +++--- .../Private/CesiumTextureUtility.h | 8 +-- .../Private/ExtensionImageCesiumUnreal.h | 3 +- .../Tests/CesiumTextureUtility.spec.cpp | 59 ++++++++++++++----- 4 files changed, 61 insertions(+), 28 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index 2e47c46b7..6d2a7d0e8 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -366,7 +366,7 @@ bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { } TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - const CesiumGltf::ImageCesium& image, + CesiumGltf::ImageCesium& image, const CesiumGltf::Sampler& sampler, bool sRGB) { return loadTextureAnyThreadPart( @@ -411,7 +411,7 @@ static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { } TUniquePtr loadTextureAnyThreadPart( - const CesiumGltf::ImageCesium& image, + CesiumGltf::ImageCesium& image, TextureAddress addressX, TextureAddress addressY, TextureFilter filter, @@ -421,15 +421,20 @@ TUniquePtr loadTextureAnyThreadPart( std::optional overridePixelFormat) { // The FCesiumTextureResource for the ImageCesium should already be created at // this point, if it can be. - const ExtensionImageCesiumUnreal* pExtension = - image.getExtension(); - check(pExtension != nullptr); - if (pExtension == nullptr || pExtension->getTextureResource() == nullptr) { + const ExtensionImageCesiumUnreal& extension = + ExtensionImageCesiumUnreal::getOrCreate( + CesiumAsync::AsyncSystem(nullptr), + image, + sRGB, + useMipMapsIfAvailable, + overridePixelFormat); + check(extension.getFuture().isReady()); + if (extension.getTextureResource() == nullptr) { return nullptr; } auto pResource = FCesiumTextureResource::CreateWrapped( - pExtension->getTextureResource(), + extension.getTextureResource(), group, filter, addressX, diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 4a9b3bbe7..472920fc7 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -117,17 +117,17 @@ TUniquePtr loadTextureFromModelAnyThreadPart( * and can be empty. */ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - const CesiumGltf::ImageCesium& image, + CesiumGltf::ImageCesium& image, const CesiumGltf::Sampler& sampler, bool sRGB); /** * @brief Does the asynchronous part of renderer resource preparation for - * a texture. The given image _must_ be prepared before calling this method by + * a texture.The given image _must_ be prepared before calling this method by * calling {@link ExtensionImageCesiumUnreal::getOrCreate} and then waiting * for {@link ExtensionImageCesiumUnreal::getFuture} to resolve. This method * should be called in a background thread. - * + * * @param imageCesium The image. * @param addressX The X addressing mode. * @param addressY The Y addressing mode. @@ -141,7 +141,7 @@ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( * @return The loaded texture. */ TUniquePtr loadTextureAnyThreadPart( - const CesiumGltf::ImageCesium& image, + CesiumGltf::ImageCesium& image, TextureAddress addressX, TextureAddress addressY, TextureFilter filter, diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h index 88ea0b16a..a9aeeda21 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h @@ -45,7 +45,8 @@ struct ExtensionImageCesiumUnreal { * `FTextureResource` from it is kicked off. On successive invocations * (perhaps from other threads), the existing instance is returned. It is safe * to call this method on the same `ImageCesium` instance from multiple - * threads simultaneously. + * threads simultaneously as long as no other thread is modifying the instance + * at the same time. * * To determine if the asynchronous `FTextureResource` creation process has * completed, use {@link getFuture}. diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp index 0e9cac98f..2fd0b54bf 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp @@ -2,8 +2,10 @@ #include "CesiumTextureUtility.h" #include "CesiumAsync/AsyncSystem.h" +#include "ExtensionImageCesiumUnreal.h" #include "Misc/AutomationTest.h" #include "RenderingThread.h" +#include #include #include @@ -18,12 +20,14 @@ BEGIN_DEFINE_SPEC( EAutomationTestFlags::ProductFilter | EAutomationTestFlags::NonNullRHI) std::vector originalPixels; std::vector originalMipPixels; +std::vector expectedMipPixelsIfGenerated; SharedAsset imageCesium; void RunTests(); void CheckPixels( - const IntrusivePointer& pRefCountedTexture); + const IntrusivePointer& pRefCountedTexture, + bool requireMips = false); void CheckSRGB( const IntrusivePointer& pRefCountedTexture, bool expectedSRGB); @@ -62,6 +66,20 @@ void CesiumTextureUtilitySpec::Define() { imageCesium->pixelData.data(), originalPixels.data(), originalPixels.size()); + + ImageCesium copy = *imageCesium; + CesiumGltfReader::GltfReader::generateMipMaps(copy); + + expectedMipPixelsIfGenerated.clear(); + + if (copy.mipPositions.size() >= 2) { + expectedMipPixelsIfGenerated.resize(copy.mipPositions[1].byteSize); + for (size_t iSrc = copy.mipPositions[1].byteOffset, iDest = 0; + iDest < copy.mipPositions[1].byteSize; + ++iSrc, ++iDest) { + expectedMipPixelsIfGenerated[iDest] = uint8_t(copy.pixelData[iSrc]); + } + } }); RunTests(); @@ -117,7 +135,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture = loadTextureGameThreadPart(pHalfLoaded.Get()); - CheckPixels(pRefCountedTexture); + CheckPixels(pRefCountedTexture, true); CheckSRGB(pRefCountedTexture, false); CheckAddress( pRefCountedTexture, @@ -141,7 +159,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture = loadTextureGameThreadPart(pHalfLoaded.Get()); - CheckPixels(pRefCountedTexture); + CheckPixels(pRefCountedTexture, true); CheckSRGB(pRefCountedTexture, true); CheckAddress( pRefCountedTexture, @@ -167,7 +185,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture = loadTextureGameThreadPart(pHalfLoaded.Get()); - CheckPixels(pRefCountedTexture); + CheckPixels(pRefCountedTexture, false); CheckSRGB(pRefCountedTexture, false); CheckAddress( pRefCountedTexture, @@ -200,7 +218,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture = loadTextureGameThreadPart(model, pHalfLoaded.Get()); - CheckPixels(pRefCountedTexture); + CheckPixels(pRefCountedTexture, true); CheckSRGB(pRefCountedTexture, true); CheckAddress( pRefCountedTexture, @@ -251,7 +269,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture2 = loadTextureGameThreadPart(model, pHalfLoaded2.Get()); - CheckPixels(pRefCountedTexture1); + CheckPixels(pRefCountedTexture1, true); CheckSRGB(pRefCountedTexture1, true); CheckAddress( pRefCountedTexture1, @@ -260,7 +278,7 @@ void CesiumTextureUtilitySpec::RunTests() { CheckFilter(pRefCountedTexture1, TextureFilter::TF_Default); CheckGroup(pRefCountedTexture1, TextureGroup::TEXTUREGROUP_World); - CheckPixels(pRefCountedTexture2); + CheckPixels(pRefCountedTexture2, false); CheckSRGB(pRefCountedTexture2, false); CheckAddress( pRefCountedTexture2, @@ -300,7 +318,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture = loadTextureGameThreadPart(model, pHalfLoaded.Get()); - CheckPixels(pRefCountedTexture); + CheckPixels(pRefCountedTexture, true); CheckSRGB(pRefCountedTexture, true); CheckAddress( pRefCountedTexture, @@ -349,7 +367,7 @@ void CesiumTextureUtilitySpec::RunTests() { IntrusivePointer pRefCountedTexture = loadTextureGameThreadPart(model, pHalfLoaded.Get()); - CheckPixels(pRefCountedTexture); + CheckPixels(pRefCountedTexture, true); CheckSRGB(pRefCountedTexture, true); CheckAddress( pRefCountedTexture, @@ -376,7 +394,8 @@ void CesiumTextureUtilitySpec::RunTests() { } void CesiumTextureUtilitySpec::CheckPixels( - const IntrusivePointer& pRefCountedTexture) { + const IntrusivePointer& pRefCountedTexture, + bool requireMips) { TestNotNull("pRefCountedTexture", pRefCountedTexture.get()); TestNotNull( "pRefCountedTexture->getUnrealTexture()", @@ -426,27 +445,35 @@ void CesiumTextureUtilitySpec::CheckPixels( TestEqual("pixel-alpha", readPixels[i].A, originalPixels[i * 4 + 3]); } + if (requireMips) { + TestTrue("Has Mips", !readPixelsMip1.IsEmpty()); + } + if (!readPixelsMip1.IsEmpty()) { + std::vector& pixelsToMatch = originalMipPixels.empty() + ? expectedMipPixelsIfGenerated + : originalMipPixels; + TestEqual( "read buffer size", readPixelsMip1.Num() * 4, - originalMipPixels.size()); + pixelsToMatch.size()); for (size_t i = 0; - i < readPixelsMip1.Num() && (i * 4 + 3) < originalMipPixels.size(); + i < readPixelsMip1.Num() && (i * 4 + 3) < pixelsToMatch.size(); ++i) { - TestEqual("mip pixel-red", readPixelsMip1[i].R, originalMipPixels[i * 4]); + TestEqual("mip pixel-red", readPixelsMip1[i].R, pixelsToMatch[i * 4]); TestEqual( "mip pixel-green", readPixelsMip1[i].G, - originalMipPixels[i * 4 + 1]); + pixelsToMatch[i * 4 + 1]); TestEqual( "mip pixel-blue", readPixelsMip1[i].B, - originalMipPixels[i * 4 + 2]); + pixelsToMatch[i * 4 + 2]); TestEqual( "mip pixel-alpha", readPixelsMip1[i].A, - originalMipPixels[i * 4 + 3]); + pixelsToMatch[i * 4 + 3]); } } } From 1777db071aa86232a2e3a740fd2859f953930459 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Mon, 30 Sep 2024 20:58:06 +1000 Subject: [PATCH 10/23] Formatting. --- Source/CesiumRuntime/Private/CesiumTextureUtility.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 472920fc7..97ee85e43 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -127,7 +127,7 @@ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( * calling {@link ExtensionImageCesiumUnreal::getOrCreate} and then waiting * for {@link ExtensionImageCesiumUnreal::getFuture} to resolve. This method * should be called in a background thread. - * + * * @param imageCesium The image. * @param addressX The X addressing mode. * @param addressY The Y addressing mode. From b03d1f6320766ede28fd782799e3bd53c6244dc2 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 1 Oct 2024 14:29:27 -0400 Subject: [PATCH 11/23] SharedImages test working on CI --- Resources/Tests/SharedImages/plane-0-0.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-0-1.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-0-2.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-0-3.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-0-4.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-0-5.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-0-6.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-0-7.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-0-8.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-0-9.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-1-0.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-1-1.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-1-2.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-1-3.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-1-4.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-1-5.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-1-6.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-1-7.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-1-8.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-1-9.glb | Bin 0 -> 1504 bytes Resources/Tests/SharedImages/plane-2-0.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-2-1.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-2-2.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-2-3.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-2-4.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-2-5.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-2-6.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-2-7.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-2-8.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-2-9.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-0.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-1.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-2.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-3.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-3-4.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-5.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-6.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-3-7.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-3-8.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-3-9.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-4-0.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-4-1.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-4-2.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-4-3.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-4-4.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-4-5.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-4-6.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-4-7.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-4-8.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-4-9.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-5-0.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-5-1.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-5-2.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-5-3.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-5-4.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-5-5.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-5-6.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-5-7.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-5-8.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-5-9.glb | Bin 0 -> 1504 bytes Resources/Tests/SharedImages/plane-6-0.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-6-1.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-6-2.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-6-3.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-6-4.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-6-5.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-6-6.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-6-7.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-6-8.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-6-9.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-7-0.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-7-1.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-7-2.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-7-3.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-7-4.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-7-5.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-7-6.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-7-7.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-7-8.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-7-9.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-8-0.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-8-1.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-8-2.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-8-3.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-8-4.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-8-5.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-8-6.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-8-7.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-8-8.glb | Bin 0 -> 1500 bytes Resources/Tests/SharedImages/plane-8-9.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-9-0.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-9-1.glb | Bin 0 -> 1504 bytes Resources/Tests/SharedImages/plane-9-2.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-9-3.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-9-4.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-9-5.glb | Bin 0 -> 1504 bytes Resources/Tests/SharedImages/plane-9-6.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-9-7.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/plane-9-8.glb | Bin 0 -> 1508 bytes Resources/Tests/SharedImages/plane-9-9.glb | Bin 0 -> 1512 bytes Resources/Tests/SharedImages/texture0.png | Bin 0 -> 58902 bytes Resources/Tests/SharedImages/texture1.png | Bin 0 -> 57706 bytes Resources/Tests/SharedImages/tileset.json | 2228 +++++++++++++++++ .../Private/Tests/Cesium3DTileset.spec.cpp | 20 +- .../Private/Tests/CesiumTestHelpers.cpp | 1 + 105 files changed, 2241 insertions(+), 8 deletions(-) create mode 100644 Resources/Tests/SharedImages/plane-0-0.glb create mode 100644 Resources/Tests/SharedImages/plane-0-1.glb create mode 100644 Resources/Tests/SharedImages/plane-0-2.glb create mode 100644 Resources/Tests/SharedImages/plane-0-3.glb create mode 100644 Resources/Tests/SharedImages/plane-0-4.glb create mode 100644 Resources/Tests/SharedImages/plane-0-5.glb create mode 100644 Resources/Tests/SharedImages/plane-0-6.glb create mode 100644 Resources/Tests/SharedImages/plane-0-7.glb create mode 100644 Resources/Tests/SharedImages/plane-0-8.glb create mode 100644 Resources/Tests/SharedImages/plane-0-9.glb create mode 100644 Resources/Tests/SharedImages/plane-1-0.glb create mode 100644 Resources/Tests/SharedImages/plane-1-1.glb create mode 100644 Resources/Tests/SharedImages/plane-1-2.glb create mode 100644 Resources/Tests/SharedImages/plane-1-3.glb create mode 100644 Resources/Tests/SharedImages/plane-1-4.glb create mode 100644 Resources/Tests/SharedImages/plane-1-5.glb create mode 100644 Resources/Tests/SharedImages/plane-1-6.glb create mode 100644 Resources/Tests/SharedImages/plane-1-7.glb create mode 100644 Resources/Tests/SharedImages/plane-1-8.glb create mode 100644 Resources/Tests/SharedImages/plane-1-9.glb create mode 100644 Resources/Tests/SharedImages/plane-2-0.glb create mode 100644 Resources/Tests/SharedImages/plane-2-1.glb create mode 100644 Resources/Tests/SharedImages/plane-2-2.glb create mode 100644 Resources/Tests/SharedImages/plane-2-3.glb create mode 100644 Resources/Tests/SharedImages/plane-2-4.glb create mode 100644 Resources/Tests/SharedImages/plane-2-5.glb create mode 100644 Resources/Tests/SharedImages/plane-2-6.glb create mode 100644 Resources/Tests/SharedImages/plane-2-7.glb create mode 100644 Resources/Tests/SharedImages/plane-2-8.glb create mode 100644 Resources/Tests/SharedImages/plane-2-9.glb create mode 100644 Resources/Tests/SharedImages/plane-3-0.glb create mode 100644 Resources/Tests/SharedImages/plane-3-1.glb create mode 100644 Resources/Tests/SharedImages/plane-3-2.glb create mode 100644 Resources/Tests/SharedImages/plane-3-3.glb create mode 100644 Resources/Tests/SharedImages/plane-3-4.glb create mode 100644 Resources/Tests/SharedImages/plane-3-5.glb create mode 100644 Resources/Tests/SharedImages/plane-3-6.glb create mode 100644 Resources/Tests/SharedImages/plane-3-7.glb create mode 100644 Resources/Tests/SharedImages/plane-3-8.glb create mode 100644 Resources/Tests/SharedImages/plane-3-9.glb create mode 100644 Resources/Tests/SharedImages/plane-4-0.glb create mode 100644 Resources/Tests/SharedImages/plane-4-1.glb create mode 100644 Resources/Tests/SharedImages/plane-4-2.glb create mode 100644 Resources/Tests/SharedImages/plane-4-3.glb create mode 100644 Resources/Tests/SharedImages/plane-4-4.glb create mode 100644 Resources/Tests/SharedImages/plane-4-5.glb create mode 100644 Resources/Tests/SharedImages/plane-4-6.glb create mode 100644 Resources/Tests/SharedImages/plane-4-7.glb create mode 100644 Resources/Tests/SharedImages/plane-4-8.glb create mode 100644 Resources/Tests/SharedImages/plane-4-9.glb create mode 100644 Resources/Tests/SharedImages/plane-5-0.glb create mode 100644 Resources/Tests/SharedImages/plane-5-1.glb create mode 100644 Resources/Tests/SharedImages/plane-5-2.glb create mode 100644 Resources/Tests/SharedImages/plane-5-3.glb create mode 100644 Resources/Tests/SharedImages/plane-5-4.glb create mode 100644 Resources/Tests/SharedImages/plane-5-5.glb create mode 100644 Resources/Tests/SharedImages/plane-5-6.glb create mode 100644 Resources/Tests/SharedImages/plane-5-7.glb create mode 100644 Resources/Tests/SharedImages/plane-5-8.glb create mode 100644 Resources/Tests/SharedImages/plane-5-9.glb create mode 100644 Resources/Tests/SharedImages/plane-6-0.glb create mode 100644 Resources/Tests/SharedImages/plane-6-1.glb create mode 100644 Resources/Tests/SharedImages/plane-6-2.glb create mode 100644 Resources/Tests/SharedImages/plane-6-3.glb create mode 100644 Resources/Tests/SharedImages/plane-6-4.glb create mode 100644 Resources/Tests/SharedImages/plane-6-5.glb create mode 100644 Resources/Tests/SharedImages/plane-6-6.glb create mode 100644 Resources/Tests/SharedImages/plane-6-7.glb create mode 100644 Resources/Tests/SharedImages/plane-6-8.glb create mode 100644 Resources/Tests/SharedImages/plane-6-9.glb create mode 100644 Resources/Tests/SharedImages/plane-7-0.glb create mode 100644 Resources/Tests/SharedImages/plane-7-1.glb create mode 100644 Resources/Tests/SharedImages/plane-7-2.glb create mode 100644 Resources/Tests/SharedImages/plane-7-3.glb create mode 100644 Resources/Tests/SharedImages/plane-7-4.glb create mode 100644 Resources/Tests/SharedImages/plane-7-5.glb create mode 100644 Resources/Tests/SharedImages/plane-7-6.glb create mode 100644 Resources/Tests/SharedImages/plane-7-7.glb create mode 100644 Resources/Tests/SharedImages/plane-7-8.glb create mode 100644 Resources/Tests/SharedImages/plane-7-9.glb create mode 100644 Resources/Tests/SharedImages/plane-8-0.glb create mode 100644 Resources/Tests/SharedImages/plane-8-1.glb create mode 100644 Resources/Tests/SharedImages/plane-8-2.glb create mode 100644 Resources/Tests/SharedImages/plane-8-3.glb create mode 100644 Resources/Tests/SharedImages/plane-8-4.glb create mode 100644 Resources/Tests/SharedImages/plane-8-5.glb create mode 100644 Resources/Tests/SharedImages/plane-8-6.glb create mode 100644 Resources/Tests/SharedImages/plane-8-7.glb create mode 100644 Resources/Tests/SharedImages/plane-8-8.glb create mode 100644 Resources/Tests/SharedImages/plane-8-9.glb create mode 100644 Resources/Tests/SharedImages/plane-9-0.glb create mode 100644 Resources/Tests/SharedImages/plane-9-1.glb create mode 100644 Resources/Tests/SharedImages/plane-9-2.glb create mode 100644 Resources/Tests/SharedImages/plane-9-3.glb create mode 100644 Resources/Tests/SharedImages/plane-9-4.glb create mode 100644 Resources/Tests/SharedImages/plane-9-5.glb create mode 100644 Resources/Tests/SharedImages/plane-9-6.glb create mode 100644 Resources/Tests/SharedImages/plane-9-7.glb create mode 100644 Resources/Tests/SharedImages/plane-9-8.glb create mode 100644 Resources/Tests/SharedImages/plane-9-9.glb create mode 100644 Resources/Tests/SharedImages/texture0.png create mode 100644 Resources/Tests/SharedImages/texture1.png create mode 100644 Resources/Tests/SharedImages/tileset.json diff --git a/Resources/Tests/SharedImages/plane-0-0.glb b/Resources/Tests/SharedImages/plane-0-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..b3ac6b99d5b04e561dd1493cad7d5d7fae3e5415 GIT binary patch literal 1500 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 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-0-5.glb b/Resources/Tests/SharedImages/plane-0-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..b81e52b3c9da06b1f6fc10699d0b4dab12b3084d GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE=A8edjw@#p(R!O4GETZ#2z?bvyGX8-&ANYo#b`9h@j zX{4uw%PnYdsj9R=G06$Xf^p=b+Zbwo9dVXjCqfjZ*XhJmBvoKQDLdcr8pmlzT>_3Z z;U#5IC}bNZ+JeKX8Jfm`P8gFs776k!*OZL=UODWYOMN3F-+>|h0`pk5l;ghZIr~hV zL9M1#E!KT|jJ1D*?KSQDFIdY2qC=;h6Q^w%Kh1DV>d-4rA#H*@iHZ};DDqg=jAo=B zl=^B%swFVPf^dq{8e0VXjtHElG@P<3PI4GW;Vw>#1mCe0%r)}w`=34!A%qpHf|ShYimZ?qfyYGo zPs*eTyx=sWg03483&Ckn2`O&Sy+0exNB#slMn@BWdeCC)ei``w^y60(S^%iQ zV+xOuDu0`sT2St|Th-t>2zACX;8*8@k_}VDRM!yrCaP6ZpQ;u3)`}@A2>Z`^&P_zjP)Mhi8}e IE&lxM7gtxa`2YX_ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-0-6.glb b/Resources/Tests/SharedImages/plane-0-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..0927e2f83f5a3700e80fa79ec3b52ec82328e3d3 GIT binary patch literal 1508 zcmb_cZHn745Oufo9znm2_Iw` z?0j!qN)5I(lHR=0%p1k&?E1z=-BB zvKL$Pn8NuB3UFLgbaoW?gTA*Ta4g%DuWi|TDs8K|Q&RVTkOG->E{V@b<=iPLBkTyXcMa7A26?rUc zMl+%tMQ?4(X$j1*Ae`bu_H zduEzi;%L4k(0BG0h-$x$2Ps&@8!I)?qOh&z1bW(T0kqvsL=e!O#iIfM2Z(j`K3bLWy?XwYs)^I;t<_WgvqsaaN=t zY#VN8aMd!wO?+kwL$;@ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-0-7.glb b/Resources/Tests/SharedImages/plane-0-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..42f64e1c345b96d513ebfb5a46ff912d86822503 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$AjzCF^PTTpCraiwSDL2%e5Gmc;JuxBlMTW_K#GF#0=bI~^2;zJ{GO5r$U}Xl|T^$LfMYDcy)SDt5rT7tyI!jp53|8iH zs~=NXe?b9;Yl_Z};(pL~b_AAT>hd)Wb5B*tN_R?X{|{0ilg1_S8L8Brl0u^Xn9OG) zwNE2GC0uSng9}xq4T?xcI2Mc}7u`ls^H#`N+KPq93%A{lsEAA7fKs-<;WdtuwmJkH zYr+f4pisy*Otb}uRWsC$0i7@+StMfQS{+j|?tA61cP{mfjC==%^b5=**-}pDZP(dn z>I`btrE0L9H^*4}H`s37zW;)?OdvXR+BtFBmhsaRN2Ch9w62@Ar~?^AAIS1-t^;F6IuYM z!D9-SkSc$Zn@UjbbhfI&GZ3nbrNFQD1;<&DV4=Epacx|i@^RE)%G*FXTi`TLptI%m zrsM$TL!k?~sw_6PD@a|(YOBUq2v+hyg2AI!>j&JWABGe7zR<4VeF@*==llE1veZAf OClQBxm(~sbeD4>AHMMX6 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-0-8.glb b/Resources/Tests/SharedImages/plane-0-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..307a4df5d40b7bc3026355f61227d9f19fc50ddb GIT binary patch literal 1500 zcmb_cTZ-E-6m_R`A3@*7@v9;APf61ZWJuy^k`9FsitH%1iftKL&dd-3T|k%E#q>(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ?_YuS#DLRMz5-O-Uyt7z8mje1k0qZB`ZQ)dY?n!(Im z?(|a%>n|w4aZTawDINxWXHQ@mrY?tRm^_WvLSGU;3rUy#bRb5bbOAB*`y zruKQJ=Y-20tl&a*X^SF~5sn4p$U}D#^t`p^EN#U?a7@t~Ds?OfB1%zO`q@(au(DJjQ&*KrP& zx`0}Bs~W7^J;mC;!FFo-{tMPJf#^7B=PYPj=1)@`kt+3)Qz)AtPom_+H1aHxJ)JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2`K~ED zgo~kYg`8Es8m|?kZeX=l<0}L!c__gUOX~-`p&y15_`cF^V7!L!>F@jd>$Y5fX-^`K L_pZ$w{N>&+N5Hev literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-0-9.glb b/Resources/Tests/SharedImages/plane-0-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..e351c781242929e76e0185d40602d68e0c1697dc GIT binary patch literal 1508 zcmb_c>x!E|6t-LX9$|iq@sfr{3MEamkR^%RBwY$2l+lbjMI9N(be9n51N1TbAU!i` z?%S49L7j7EzVn^yM9JdzM$@#PuQcr~ymxbNx@KTz@wxu%Z% zCqcY0EQnBIz|x#$Bom9LoFMnLWji_wSee0gS4To=(R|P!_h(2)DSiZ_&I(pEgO$13 z>8BLdUr>PInxeC(co+_xJ%MGIx_nK;JWy4#(w&pq|AQ3Bq;W}nK`M3Uq>!jTCi9s{ z?ej>_30FJN;6hbtiz1Q{js@e$MRyU@ycKemwqhaj!fm%BD&o>Npp@;ec!Q&)tquXl zhVX(iC={{{6YaoZ)eLoGKqrhz7Ks?SR>zc#hh91Eoojs~Bj1A|{Q~nywv^N9b)7?| zE}&Ljss`J6bBeWpgYDMs`!86_1fpZ7oinFx89z;NM5@qBP9bfAJc^PN%gD1x){Lg4 z8kFj2TdE~6#e#5(lM0*rd_n|H5*o}{8O0flBXVI*a6twvVf+>cs+!BD(d)k~L33tN zQA-@n)&%Cx+yPN-x3M6Bh&LohS~!c7JjN5YhPg)GegEU9A%w7IrJs;FU6VC(L-3d= z|49+of#;m2RM1T=Vj(#7OCiNAy7%Uz#mJjN$LMJ4%_jXZav_t8;pc(p%|3iFp#^{% zJf?67sq#0usRZRtXQvuG1EI=T3jAtcaGVth7OHEPJ)>vBW2w26mx1)Pz-gX9W6Rr3 z$pMUq0vB>s8Ei~fkh+Z3R*kO^tmJ_NgF~%W>j&JW??+SkzR+&qeF@*w=li?sveZAf OClSYc*VYaGa_<+70JUNO literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-0.glb b/Resources/Tests/SharedImages/plane-1-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..53f8c61939e17278517655b238cb899b2652d9de GIT binary patch literal 1500 zcmb_c+lt#T5OufoKNdZk#J7glFC|U0kR=;$lXNMBP-I88Rcy=1a=J?h^aJ{d{g{p< zJNIo%X~2#~(wQ@tGfuMk&6QyoKVKP!2hZ&+m~0S^B2rb1SIA#%P*}$?;SZF2L%wCA z@JWy$jw>RR8L_-%1u4Y*sU*mMZ9A@sB32i0+%u6-yJ$A(kNZ<(q8vYhQ+Ej~TENO$ zZp~u~`!6WKaYNzlC?19bcSm4%EK@#K$J$eATkV~aI{$+d$fR>gd`2qwPD!Cqe=O!R znL4MLo)Rv%u!Aerr433*K{ytSBOl!+(DUnOo~LL$hSR9GVZlT|Z+q^( zQfE-RX;q8$-W+3{-(Y)c))t5wzpV!eM7$+A*oC_|D^q;ORxsBnxbJ`bG=vmZtPV3WqbssPehd*4 z6+fxcCh?NfoC>;bOe_SaVJ)P&LHEIIG#>>MP>hKt!St>_Mm|(>KKwigg6W4Z7Dxc7 zAz}(2A=Uo2Hri0(cv~g#0t{WT9Qf6_;JBzVER<>&dK_&%^H7`$=Vb<(mLE15b``;7 zD4bepU#(Y|Sh&)3P#-mTjbJs8Bp71(0pHN~qY1oU7+3JTg!l39`@74w+<)#&A`Z_k Jty}#0*)LCbv+)1` literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-1.glb b/Resources/Tests/SharedImages/plane-1-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..655d287f64c970ee9693547e4a96e00af035ded7 GIT binary patch literal 1500 zcmb_c+lt#T5OufoKZ2f3;!7G@zmzo1LY8d2P12>Xgd#hNtzuh7lG9y6pdZjr?8kH@ z*|~39N&|K@lFppDoNQ5WK~c3@5q?j}HxxKF zik>71S1UoJHe;5TtRRJ)Ka~UpuU*f#QOv3Wj(awedKb+G{c(SaY?R|iaOy8$MGIIt zi>-Z3;r;~$IIbzWJBs_^z~2$LZO2xR({}b$#@2hMq~8A^1v2Ga5ucH&y;D*s)E|ra zOs3vxrl*99E$m>SyR<sB?`?wez*_Zw`lmhZn{T?dE`gZ58?_Ei2f#|bg1SDZrGBzcq-Cr-O864f)B z6Ei60YDaA=V2&l>6lVroM*NmYoMkkgvMNan7)KPqoM1r)EMxo{$GV#jjnM0V)q>{E z+)yhVEtUl4&e;M{?YHru1dDh>((HmexQnwg#kXt;bB)5g{>M*4NMXsUC?hkvBuf-X zUJ)HW5ow)w$!Sg{UDqa-lGCV?%G{v4a5kEc!U+h*Mw4)Q+aIF<3OOHs9)#ia!xsk> z0L&0Dg>R7Vep4D_=xDlIt?vR1Q?MNP^||D@5E+(Qv``%vrqMMc5490EFEfy|`mU+4 zMGS+Xae8HZHC|z0;Y!^=b5!FEf>k_LV2Gvl1HPf}M-zCzu&&^F3Gd_I_ji|Vwg23i NLL8o5S~vLfvtK*tv&H}b literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-2.glb b/Resources/Tests/SharedImages/plane-1-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..b5047f4619f4d6687c104577a3971d9153b3225b GIT binary patch literal 1500 zcmb_c+lt#T5OufoKNdY3$Cos;ekp01g)G^4o1{x2gd#h#tzuh7lG9y6pdZjr?8kH@ z+qrLBN&|K@lFppDoN|i(`1#5(y71i2g2@KqC?Y~IE|9<2ps-j2YrY*Rh9W$&qKTkoBcy8nX|$dq$Md`7DFPD!Cqe=O!R znYyQ$o)Rv%u!Du}(gr1@ARJ4^k&kW@==pWbS$>^LSqi_?NvKS#(1KQWzT!1bvW~t4 z9BaY_WzZ<(S~l83z*-EAFhB`oQY11(zSFf8<6bL=+PS0~mH7?~<(HTzN>X0;ZO_|R z>I~{Mt!lB|H^*4_H`rby-+#e6HV_>K?VSYes{Cn=6H=#MaSCOV@5&Aep?R;h>MP>hKt!St>_Mm|(>KKwigg6W4ZHb?-d zAz}(2A>IDAHnpL{>29^a3oz=6<-o7cCC7!xu+*wuxRwj!=+rY0#Su6!Gtjj9u&J;` z1e2k0dR6;sy~4!8m8OIBQG>4$tm2UZLo7ew8~T1Uf%gmJ3Z9qnKK^}wciC3^&z&j6 M;n}5ii$6d61yf_Q^#A|> literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-3.glb b/Resources/Tests/SharedImages/plane-1-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..1c796fb9019f726c22d4563ca4cac0c1276d57fb GIT binary patch literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZ4Qp zeWlKzPTi^o+j(=0b$^5H*7E%qtYZVwVbI=5(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aiqFAZ-id|%N8_u zW|~^!Xuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_CT}%ry$`dY?WIAcZBX!jw$uk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&61=Hbd7>q$MCK?BmQSTP{P{`TfOFsxEAHUk5 z0APlIDSU%e_nXpaLq*fsDt+f*=z?Xyug(R>d6{CNL_7B^&z6r1GwJBThYFQAD^d`) z{JP2D%Lv9pq18%zY<$Dm!j-y>`l!Zh1gm%?!4Ofa)%pS7(htKi{Jt=*;C%_d$Itio Tmu0JOM_w$y!B`_5gdtJ|i(`1#5(Ja}$r!DNGQ6cHg97sy|1P*}w=;SZF2L%waI z@JW&&js=n0j96Z>f)sN8R1)OBcHFLsB32b}+%u8XyJ$A(kNZ<(q8vYh)9w;hw1Abp z+?vM}&R-5r5r*`|7I%idGfw%$7>b^iw`kSXVi_>5HTosvSK{#eXs zGIdWgJtbUjVFwG{r433*K{%F-BOl!+(DUn=WF378 zIM##<%AirmwQRJ7fVCJJVSp0Gq)23ne8;mD<6bL=+PS0~mH7?~<(HTzN>W|#ZLhnp z)EU%iTGe8`H^*4_H`rby-+#e6HV_>K-8~7~Rr%8#C!|ij;uOjz$)l_|ajddPRL^Km z>OrZmcGR{4=2#L=aaLo?klzuBvy4VlRwZcx7eU>EM|q!%8Z1gYJXbXg&%ipcoTPg6Um+Y>)s@ zL&Ow5Lc0BJZE8b@<88IT3oz=6<-o7cCC7!xu+*wuc$Np_=+rY0#Su6!Gtjj9u&J;` z1e2k0dR6;sy~4!8m8OIBQG>4$tm2UZLo7ew8~T1Uf%gmJ3Z9qnKK^}wciC3^&z&j6 M;n}5ii$6d61y@hA`2YX_ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-5.glb b/Resources/Tests/SharedImages/plane-1-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..fed618ea974ede9b0bd7c06fca5a1bf5ead9ba57 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|n48k(PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-gv1~_2AuBUD?&?UWT{P?WM!hN0QHr0ysk4L?&0u9N zcls%X^%oT2xTbLT6c2;GvnQ|&Qa7@t~Ds?OfB1%zO`q@(au(DJjQ&*L4n+ zx`0}Bs~W8P_7rRX2HUOW`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE;>dBrqQZK9%@tIG*3X%^1CL( zEwJ_x=5KTkgMf OCK1PH*VYaG^6VEw$g|D> literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-6.glb b/Resources/Tests/SharedImages/plane-1-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..783d80d0007864368467dff5e4434c185df6c79b GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dA^o*V<9~XwwswW>xRN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%a}wQ&Fd literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-7.glb b/Resources/Tests/SharedImages/plane-1-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..d143bd58e73a56fd7dc7299f2eb7f08e5aee1422 GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|I5F03L!nsff{U81+NB^;CUH!rB$+UoxVwnZ2k2w=L3(GB z_`Pi@4MgY8%(>@&oSQJ7-CP-l@$;2oyo2|4>Ww#u1Od%+E^_3~H^?tmD=O|8eLyZY zk^dyAw_4>?YBS(T##5Tg*;7W5``T_fCJK0w!g0q$QtzT^uRH8ckckrV2u_^^tXK*w zyx5w@6!u?Gfa8jyy`#AA_naMpZDCWr*us0N($;&Yq^0ifm3;pNYhxff4B9yfx~1}G2?=SLdLbB;P0~kM2#T#N4OP!r zLd!uZuQt`T0wzRK!AM+UGhd9TByr4w2`|DZg>ghK%n8Y9kH=ixkU)2HUJJeYS1qXT z%t~s7W9gE@+~F+{Reoy^O0Y=OB=s)1gS#ZoA~NDjm}}(Sbw7RXLkde?_%WTbC0!y{ zih}C!X&zO1XM!b6vUOz=DFyQjsmu+!^QME@z#D^LOf>c;qwWy7P{>*TOV9HrAHQNy z05C(q48B3S`*mqbLr2rzYJI0*lm$zGU!O}s(mW+7fXtZ9H9&6t)ws56tV|i5JO9U@OpuiAOqtW;Q-_j3*G5o$TuHbzMzsJw_ U_m^$8|J<2E9G+cT*ZA|ZUva6mMF0Q* literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-8.glb b/Resources/Tests/SharedImages/plane-1-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..c94389505158c66c3f81a64625c5672c076d41d8 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6w!Qt6OIMr$V0a=^!z&FEW1vGC`zx>iK$4cz<^eEzTq{F(~i0X z9BaZ$%AirmHcYgIfRz{;VSp0GB#%XcJj*pD<6bL=+PS0~nfVS3ZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&J);NXXw@?h#VK)Cq@Zc}VUuB( zAxwtCsa5T(^$HUUSDFsiM-9G4u!@Hg46*!xZ|H~71m3T-D|lYR`}p_${bgJ3zjP)M Mhi8}8E&lxM7hfl|0RR91 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-1-9.glb b/Resources/Tests/SharedImages/plane-1-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..3f2257e8a1d8191d1c1da07f6faeee83c945defe GIT binary patch literal 1504 zcmb_c>uTFD6!tpy9!0;6<4YDA{~1}9LP{EUNmd3S4B1g^4cju3oVA3&9$*i-N7<2V z=iY6M8Jy@y`p$PQ-*KGIZm$f(`1!^#Zs5I}2ICFFQAC7bTp)kGL1DRC5&lTYH{{zU z3ZEqjRx3fIHY1i5EGM~~Jr@M|Zynb&QN+p|jyoojdKXRmy-{z1OqAg#aOy2!MRQo$ zi>-M~;rs;!IIbz$JBr6a-`f#5mTjunw(LDsW$V3DQulw50-18Ih|fsX-YF>*>W{^I zAyfA>(^JC57Iv`EUD}|StQWnB*wPGrhGPIzTt*>~E_QEeue?h~%+Mkngl@#kkkXp>{6mMrFPOL-{4bl2a&~Bu}#B#I}k&Rz0H` zsRpIG+EUvJm|;mc#c72tLViyqPE#69SQ#fdj3e@4POu<-mNI^eBi+q;BlP-TwV=5( ztEd%@=1T%|XK#V1_S<++fu4@xZ$!S}?~RZTg`5pO_k&>a;foCl z0A>i7!Z%2FzbQ>+=xEwot?wL+s$d!L>vPF*E>bMDXy;w4Ys2HfNIKQrLxBX&iWGFM zK5i;@5y5n5uwGRT8`m(kaHZ~IbyVXk1S@%@z!1<6_>R6Gj^X!}aRu*d_&t8Uzq@R! R{g=)Z;_&R!y1}2H{Q^|?w66dF literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-0.glb b/Resources/Tests/SharedImages/plane-2-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..582ddad0f62c63992412408c7b1cdbbff783612c GIT binary patch literal 1500 zcmb_c+lt#T5OufoKZ2f(<4YP^zmzo1LY5@nCh1ZLp~#M6tJs#2i!Q>AXCm2@dc^cJ12!g{jr$O zWa^%0dQQ09!44L>OIs9^oNz1|M?SiXq35kNXIU$evJif|9aEW^wVaav)EkdKMPX-cChE8`@GaYR1M2^OT!QpRs_q`SFjgkJxv7BqL} z6}7_Ad_`dH>>Uu*ej5);u!uJ#%`UiuyErWpJZ3AHYZTn~K7JZN3M*EIDVfm~Ss`EY zlIZY>Nb0-`PBSX$rZ%yZoQ9=T<`&%tv*COgOh7Oungr8vZ-jg(W1eu#!g#46(F+z&G^$a02fa#tl3#;eGo1{_eW1_Mbab Nh~u+s>jr;$_6s}Uv&H}b literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-1.glb b/Resources/Tests/SharedImages/plane-2-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..21e6499216d916c745904fbf11f379f5bc5666a5 GIT binary patch literal 1500 zcmb_c+lt#T5OufoKNdY3$Cos;ekp01g)B+DP12>Xgd#h#tzuh7lG9y6pdZjr?8kH@ z+qrLBN&|K@lFppDoNb^iw`kSXVi_>5HTosvSK{#eXs zGIdWgJtbUjVFwG{r433*K{%F-BOl!+(DUn=WF378 zIM##<%AirmwQRJ7fVCJJVSp0Gq)23ne5Y$G#=TY!wR1@~D)Su}$}cfbl%%}w+n%?t z)EU%iTGe8^Z;r9OrZmcGR{4=2#L=aaLo?kWYxjSw^E7tCF;UaYR1M2^M6)GRALkq`Udh3cdMPEokp7 zYHEd}#fre(*;^oL{I(tx5b>7eU>EMhMD!EDkWBOfZc7=9iE!R*5q8zcbK z5HW?1kZyllo7&LfbhldI1sHY3a^TnJlH)>TSZdWmc^uQV+ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-2.glb b/Resources/Tests/SharedImages/plane-2-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..661b2c5dc047ffa113379cf11fb25a1d2d357646 GIT binary patch literal 1500 zcmb_cTaME(6b*yeN9FG*&7)M1A5=<5>Zomowv3Rf3OT8p7$tFJJ7q>yB^JOESq#@s z()R#DRC4NT``mLM$Bxta?Tuj=KVKO}2gcnjm~0V_A|eFi0{M$A3d{AH@CQo1A>TGp z_#{cNUJD|%8L_NjImzYxsUXOI?YN$aB39KV;Q zby2FLZMCd`8J2`ooL1N(>$~ej4IwBwL2^OT!QpRs_q`SFjgkJxv7Bpw( z6}7_Ad_~~i**hSr{WczyU=eRfnpJQIXK`92c+6ICuTgN{`}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLXc!PBJo6=NuTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&OOVs<>Q*JD?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcHvo`=~`|z@=zOrvmymatM8f$ zTSPDz8mCv4uf{73EL^D@SRK{)3c*SqDKNy+`T^h2_rnRiUl=#=yoC4Z@B6#!w%UL0 OOd*cXuB{vV<=HPoz_ZQ( literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-5.glb b/Resources/Tests/SharedImages/plane-2-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..3651e61c49c03f274df4739630870374af22d247 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&U8AephF#A*6sN>lk%Fe>hfRiE zhA<(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQSc?{tsB?`&YNSb^BZipmhZn{Z45+*LAxhGJ1T#cl89ERmx4jrBz=^npxDZ@NcD`R zv>KG^YFlk9U`iwvj3gB{55$N{k|Zpg@G^=s7)Rv6oREU{dBVjt33WH;jnM0V)q>{E ztfE#pmMtmF9o_;_?YHru1dBvN((HmexJ!~eCL_Ltxkmn7@6+c2q_E^=kkBbx(k1ew zD5(yg7IB?-E?CMWTh}I$Qm~+u%G{tke>$8E{V@o}L}Pz4>fIs_3OO5m>HGfV<5vs{ z0A>i7!8b^EzbQ>+=x91yt?vwss$ePb>vJhcRwP7f(ayV87ptdZI+jxnK2)e6X`X7jlxdMgRZ+ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-7.glb b/Resources/Tests/SharedImages/plane-2-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..b4a800a3be438c4d548546a3a6f9d345029751c6 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7uUwMDIZ6-4ZE6rC{cmaJOO3P z&zlUs2w*-GTCFONjc=G+xKj7AI;!y%f|WdwV2G&I`T^h455o!kzR<4VeF?wE&-eG2 SZMpy4nM53(U0OHz^Rr)y8MSl( literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-8.glb b/Resources/Tests/SharedImages/plane-2-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..aa370f486fdf1f14b3a66a3d04fb93f0915a7c6d GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXF1kinw+*`*d8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lgptIHh literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-2-9.glb b/Resources/Tests/SharedImages/plane-2-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..e7b47423138bb6cddbaa3d402cd3bf03e0bd438b GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mQ=(T15 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-0.glb b/Resources/Tests/SharedImages/plane-3-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..b7052e366bde9dfb5141a8e19608a2d0d6392613 GIT binary patch literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&gD3ImS@Yy)sqh;Dsfh%pltbh zlfjn}%!fj&mG;>9hPj0+bszOnjn@cP@koLpqE_n%d`mwJ$ME~YxPtd3{2o8w-(R-n R{&Qy%ad>uV-Qds9egS;lwPyeT literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-1.glb b/Resources/Tests/SharedImages/plane-3-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..9d072930811898df4fe326b11696917848dd5b9c GIT binary patch literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZMNmd3W4B1g^4cjuZoVJ9(9$=5T2icKi z=iY6M8Jy@y`p$PQ-*KGIudfWl`1#5(-obk_3nm+cqllCx<0bO%Hz=%DE5h$6c|g8x zqVP$OV6`fVP-eukg5@L^^QVF!|Fz?KCW=^T2-N?*$U?{)9JeHF3I&Zt) zzEWpUr*2h)?Yue0y1&78Yx({Q*0F);Flg^2XjkS>GaM70dc`S}O^`=XabjCV9?PE5 zjOanpSKD%10y8WKr#RKvBIIKtaGKI+%BncYVH}YUbAn6KXDQ>?I8xobZ-id|%N8_u z=9*gKXuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_Ca0%ry#bd!IfJAcZBX!j#PDk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&62D9OO7)(GgCYl7(ac_itDCB(br5^;-k6&$2 z05C(q6uv>K`%P)Ip`z(*mA-Q@bip#re}G!d|W;FP@xiMMGC@} zUpEuTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&RxqjJ9uPB literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-3.glb b/Resources/Tests/SharedImages/plane-3-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..888ce036664489bc196958e792820e62efca084c GIT binary patch literal 1512 zcmb_c>u%F96b?hYN9Es9noFr5f1s33>a;eaEfZ2zAt!Ycqa=3x6@k&^h>{?2zUpB<;O>np=Be!en{_we3KgYgF8C?aLac!~V^28GpXMfg1>56HJo z6g~+OtX3rv%8Xc6u$<&#_EZq$zjj>DL=mfUSnirgs97}a_ilR=WTFf|f>Un+Bbvj= zUTn={3g<5e2?qBqtmT#*vS1V(58m#aY%$geXeC-Hxe9s?dT~w!h&uj?=c< z1RQI^OUj^8$hB;=g@Bb9YGHs9Zb=@C1o=+KmW+F?9BSv1Ze->=FqB_l9!p7iowr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8IFlgz2X$gCdi|xII*oFk7dtj zM)X0^TibG40y8WKr#RKvBIF|?aGKI+!m2pQ;W#26&Iv9_pQVgn<4ARL-Uz+^%N8_y zW|~^!Xuc$H?(8iP)qWcfQm}|OB+V?igS|K{5wWq>fE1Rj3R5zrOR_}1 z;1yBflQOCEE;!AopzGSiLU0;ZLYf(T?l+~;hKi=MRr=1s&;`qYU#$y{^D@OkiFWQ;o-H5O+>hjyAVV&3R-_x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dAxQ44chGWXdt|uQ#RN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%UfwQm3b literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-5.glb b/Resources/Tests/SharedImages/plane-3-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..d9ac63cbf573e4342135140ac6082ab832ebf22d GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|I7w96P$<^A;G$-$b}5LINgU&pBoihRyNd{YfIem)q<1EX z-`kebKy>cRoO|xaxryT0?Tuj=KVKQfTX^rL{&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&C+P34GDC9*7#~l+%y^E&3?yx&SCQ8U7ICU4WVkxZf zVrQOG*ndF*jw_1Rp5me3bN2+cg-!Kh3m>RTTkoBdI{$+d$dq$Md_k)A&PkzAe=O!R znL6j0o)a#1utRd)r7enRN(GTzAP?O|(DTMh@T9SpGRwVYGh%XG1QxWi`IT%)6gTxH z;Mh=+GY*YHj)l<<0@h-vgaJwz(lnB5B(7!O)G*3LEEsLc0ZD8D2kQj&68Z#wRw zQWsFWYE_MGy*|Y{zrl7Y`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04wDY#rHeJib>T$}!hYIB+$zl+; z`nsv$^AN^EqxGuvSo?;tg)3DX%cBZkB6uM}1%`+kjm8i7mcAd1;rE4c1Mf@tJ$=5v UyKbxf=gt)3`0U!c#$TTO0&sk_L;wH) literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-6.glb b/Resources/Tests/SharedImages/plane-3-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..2779bf34712a29ae2800aaf1bdadb4026c2bc172 GIT binary patch literal 1512 zcmb_c+m6#P5Dkm?kCmUJG&d^93zX88x@w!HEi0s|LQd)=MoAnwPIp&TB|d-;5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^M3q372TXW4ZkL{WO3PE18o1qQUT^9`?YoOaYD z;8+u0QU;Aewqc?z1gyl+2m_QbB6%zl*WzT3v z>OrZmcI37MW>^qTaav=GfRBm5X-dN>tKuYwaYP=>2`)*WrHtR;P<35b>7eU>EMpP_SQVsXMptBoya*yD zDt=NXP2vTo85MNhm{L|g5$hQu~4dAbd9blA6It_N3HC7{-IDM&WaTD zEkANHg))RGQ9jhF_S-VR^um>0lZeB!OY0VYe)bEOUbbHV literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-7.glb b/Resources/Tests/SharedImages/plane-3-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..b3912f5656640be1c409c0e96f5cfd856ffe203b GIT binary patch literal 1512 zcmb_c+m6~W5KXu0e;_|cAs5QFd1)zSC00PTKv$Izs+?egt0azWhusxI>Id{g`xzZO z3HNQQs*wU7+h@*P&LmEzH&=#X{Cs5?@8P|j1fvbYQAC7bTp)k8L1D365`Is~1M+PX zg-?+J{}%Qn?(TlSu+wDsO8srx@jflN79#Al>x@01h@^~Yj9 zlc{@}=_%oS3p-foE^SauGQzQB9Qo)rhMqT;oTZJGl)3Pm&6vtn5n9m7<~O{?anjV6 zfMZR#pbQ#?T+2pV2w01u5(X$?K(bh_kngl?#kkkXp>{6mMrFPOL-{4&-FN{SCHV$@gEdjtxYIL3<}byDER0;+T}F7o0-bBzcqtC$^PmvFaI3 zNjWIx)u!53z!XcuDNagk9`YfPI7w(UW<|WpU>uPTbAkowvV`#)9O-V(YN1#Ess;6( zX-TbcG+PjuJ9`U6mEYQf5-j31Nxcj1;4V(`6&|t$%ry$`I-frGAcX}h!h}rdf-I0P zc|mmeM69a3b52t#>AEtpl$?fzROSZV1(W`?AB;dSCK?6fVP}ARDCD&Fr5gm}k6&$2 z05C(q6uv>a`*mqbLr2rvYJF#5lm$zHU!O~kGm&7aMLWB;u5I;rre}G2<(A_QbrLwu z6Ogz1#;FuU1Vf^I=vC>rW`N;^D^)YgqY7UlSivI&hPWC(;CuR^KZ4&E#udCT;rICY V{_e7^_Mbabh{Ll>>l%N4_6vcawle?# literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-8.glb b/Resources/Tests/SharedImages/plane-3-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..f4ac1a0480c3f7654b06afdcccae72dc8b1c56ce GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Qw~3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq678Qj-(b77eE$V&nLu(-2Zvv(it%?qUK>}v9%LTezF}_RO5MlmsK!?aR`NiCA);362YgH4kEZbZLc4+YCH$T~-``!g S<^FSL5^;QXZQbB6&wc@o4YhXw literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-3-9.glb b/Resources/Tests/SharedImages/plane-3-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..73333b2175f1e3bd3baa36d9bb94df456638efc5 GIT binary patch literal 1512 zcmb_c?T*qw6s>D~4>G@-GHsDa|0p7xumQRPZcGR<(=wDXZKs(|aW{m-2k=3C4DU=^ zzE_R0Nn!5HoO|xaxsBr4?Tuj=KVKQfJ9zJ={&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&CUfYgqqL3FU9Jfs*^)8zBy2I`SnJ6KT;M85filwl^ zi=BB&VgCgMIIbvKdy0pC&)pN)7BiiEX$}IPq&4|f$5m?a5=2x;IQQXv* zfMY{N&Nws*ITl7c2w01u5(X$?NYhBJk!QECVmxT&SUcBrqcY!vq5P7FNJ+|Vb=vNs zQWsFWYE_MGy*b4?zrnUE`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04wDXSD!SL9oYq@HHopSo2OgTxi z7}TvkaVmp6gc;E;^s01Q6Ts}km8zHJQH3uNybz%RLs*T*5BQ$GAB^Gmg>eJ#OZYu~ WzQ4V0tNrKB6yo^o+PcPHp8W!djkYBK literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-0.glb b/Resources/Tests/SharedImages/plane-4-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..17b6f8b5e8bf2f2e30eea240041d383dfbda60c6 GIT binary patch literal 1500 zcmb_c?T(W`6vZ{ZM>D?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcA=8 N5XWcN)(!sh>=!@4v&sMf literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-1.glb b/Resources/Tests/SharedImages/plane-4-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..69b3a0cc968c9af542ae3cac6890766754727527 GIT binary patch literal 1500 zcmb_c?QWYe6lI0JOM_w$y!B`_5gdtJsU*mM?YLbNMXV~|xMw1%chP*%ANOa-L^*y0r`;v2XaOsG zxiya|oWGy|#|?$IqqrXqx;p~LvQ72amc6H{ZM}C&>i!Q>AXCm2@foSwJ0*oe{jr$O zWa^$~dP=z5!VVU?OB|q!%8Z1gYJU)XfX<=pcoTPgW04%Mm|(>G5kCTg4u^JHb?-d zAz}(2A>IDAHnpL{@wQsv1sHY3a^TnJlH)>TSZdWmc^uQTyn5!LI0ENo2AWnMHWjvr zU@|mLuWDbdSD0A1(sZysYVb9JRXkE)h~)=-L*I|4@P1)j!SfQ{$G`9IF57DVxif`0 MJiD}R@#km109QA&`2YX_ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-2.glb b/Resources/Tests/SharedImages/plane-4-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..3666813a657a2c1670e48e5c7d05ea3d6c7255ba GIT binary patch literal 1500 zcmb_c?T(W`6vZ{ZM>D?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcHvsC=~-Si@=zOrvmymatM8f$ zTSPDz8mCv4uf{73EL^D@SRK{)3c*SqDKNy+`T^h2_rnRiUl=#=yoC4Z@B6#!w%UL0 OOd*cXuB{vV<=HPou(Qqp literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-3.glb b/Resources/Tests/SharedImages/plane-4-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..81484dcb5c90b81a4b52576268e3f5de7ecb0582 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dAIEG`&$JSlLttTH!RN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%UTwQm3b literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-4.glb b/Resources/Tests/SharedImages/plane-4-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..0e156d7f198d315c5ee44e5b4c61433c86cacfa1 GIT binary patch literal 1500 zcmb_c?W)r-7_LXWkCES{Yr8tMe`L-b*kN0b+dK$kNZMs>q-{!)&T|+8FThLmVtkXf z`#vh-AYI-h&-=U|PZFo|+Z)3$e!emc560arm~0V_A|eFi0{M$A3d{AH@CQo1A>TGp z_#{cNUJD|%8L_NjImzYxsUXOI?YJEiMXbzWyK5q;SJABB8}+8hL>Ybrr=2CtXbv-b zxie2GoWGy|$2Enwr+66jJ9`4hvQ0H?%RW$5wq83Yb^iw`kSXVi_<~feos&YL{#eXs zGIh^0Jtti5U#~WzZ<(S~l82z*-EoFhB_-lE*SZzT?@7@t~Ds?OfB1%6t!o@=MHPC8>`0w%a*W z>H_N2t!l8|n^Ua&8*I0h@4sLj8;FjB?wkefs{Cn&V^XDFatdXW^wVaav)EkdKMPX-cChE8`@G>xg`~Cs>d^OBuh#k?!WA5qkZvTF{)C zSJVne^A&-6XYYWh_S<++fZfX-t$!SQw{YDs4xZ2iWDTR-ZgDU za4|Gas9EK!@mfLZ23A`&zCy5)M+ywFw0^)F`hGZp?+fDw#!L8~{=UDvZmad@_7viH L@7lbuTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&U8AeJhFi}(6sN>lk%Fe>hfRiE zhA<(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQScuPLb3zK*=Lr|rB-GuUH$t!fRSTLs zvx-{bShl1vcX$g#wco~r5-bu8NwW*?;4Vq>n2h)m<{J5Ty-%M9kiwFeK|-f&Nteiz zqNF-}TEunUxnL=iY+ai~O2L9sDszMG{ONEu^v56=6OH}JsCSDzDCBJLrSJQbk6$q; z0GJ_Q2Hzmv{iZaPp`&rPTHhHMRl!o=*XL4@tVoE|qMdiGE>@3gx|UlFK2)e6X`X7j}BJN&o-= literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-7.glb b/Resources/Tests/SharedImages/plane-4-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..e1d4befddbb889c33bee1545db0938bb327038b5 GIT binary patch literal 1508 zcmb_c?TVW~6t!FW9$|iq(P*=+6iS+AAxjdsNxBq5D5Dv5iaIim*db{d}cq@8G?id6NyoK|qRv@dCMv4f4w{B>bL|2jrSM z@}C6p!muDhnE^|4mXS=%pK^lS*H*{YQNYR!j=MS%Y8TD=y-{z9bd=&paB44MMKf5L z%dLJ)Vf_UKIIbz29mV~iZ|?{!!_?(#8s?s=ven)xY3F~C0-1C!iO)#o-YF>*>W{^I zCezMorl*9KG>4LcmH4wJ<;lBa%fTMy}3GygPPAntOBH1&V zl4?+@t8KY0fhiV*Q=C-T+~;E=aFWnq%E~CtU>uPPbAk)fX9?psI8fbOG(xZcWeb`+ z^NL#HXtp9Scjgv|YQK#K2}HahIoO4}ILTu?W-FL$x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXF1kincMZ21d8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lkfV0{F literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-4-9.glb b/Resources/Tests/SharedImages/plane-4-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..b4369264d79c146b7bb3feb055fa49e08a601d8d GIT binary patch literal 1508 zcmb_c?TVW~6t!FW9$|iqQDZ|Rg_5RO$dbfuk}ic1%4kNNqK=GXx=RT30s5GIklqOJq9M=@ip5kFR=qjven)>sr^4lflNA=#22J;@0=70^~Yj9 zlc{~4={ezQ2Rpb>UD~3EWQ1eEIC9Zl1U+wsoTaT;h`eyy?TCuF^bKfb`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`OtMJC$h90(G9I*YtetDRk(uwoP=0}VBqh~xdfm>U zQWsFGZdHSI-kf6X-(b77eE$V&nLuCe#u2$NC%7O3mN0&c1J%uCBlP-TwxGGQ zsHi25W@`d-XYPQg_S<-nK*Sr8qg}X*lRU-~wuZSz-hKb$ry-=UW~HByIbD-Aazlui zsQ5_{*NNwxrc}^PZDJuf^-CemExPySqs7RZf?{+u^=6a)7`af%#qjgM^JX8um>>b5 zhKMPAgjD;R+Ej)L$Jr@?XJAwnOMzdV3y!lQ!9uBa*)w`3JeKYlPBr*Yp#rCQ0>YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mU(6wp+ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-5-0.glb b/Resources/Tests/SharedImages/plane-5-0.glb new file mode 100644 index 0000000000000000000000000000000000000000..c95fcb4cffbebdc8db0dcc30fbcd8d2523e404d6 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&avZxxx1M<@PKmQ31x?Ejn+&@Q zVKNj>t!iJbSD0A1(sZysYVb9JRXmhnh~)=-LqCir@P4IT!Sfp4$G`9IFWYker89{* MJiD}R@#km109P-w`2YX_ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-5-1.glb b/Resources/Tests/SharedImages/plane-5-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..16c81779b71e36a80f91e67bd31e0bfe891fea33 GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9znm2<4Y1Y{~1}9LP`>MNmd3W4B1g^4cju3oVJ9(9$=5ShuM*A zH`i`sEMP}R(s#ad`HtgsaeZYN#?Lo~(She?9!$3gM-dT%ae@5%Eegx^n(#YH9*}RF zD14G6Sg!?<+KgCMu$<&_@l+7xcOBO=QN+p|j(aAOdKb+H{c(SWOqAhAaOy2#MRQo$ z%bj^l;rs;!IIbx=&lGpVf%i<{ShlGi+p_mmm96(qN!|ZJ3S`Q;B0eKkd#9vOs6Q6- zg-qSkOiu}yJJ`WOcWH}ak`s<4_QEeue?nB=icknePC#kkkXp>{6mMrHmChVo0yVQ*(_&f8c2)i~!!fB+FFA#>N%AO5PHd~lW7RX7 zk!nz?t8KNdfEkvAQ=C@VBIFYyahlR-#>zO!VH}YUbAkmKu$1v@9O-V}H$t!fRSTLs zi;7y|XucvaclHj5YQK#KC0N88l4cj&!Cjmd37)VO%ry#b`=34!A%zty!<5YFimZ?? zc}aBmL?m_I1*aL6bW@vHN>0O4Dszi&gZXGN3Z@_!6HSBJq(4SJ6ml{AG6;g%$FDXh z0GJ_Q3f~~z{iZaPp`+>Sw7zpNs)A*}ug@jNxk$0pqJ`=>X4mRgBM-F^I4e?+wEC{8 zutfxep>cXu`D(nvz`~Wffz?rsuMn){kpe?3tsn3W{VuTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&wqfgCqg&5B6sN>lk%Fe>hfRiE zhAcRoO|xaxryT0?Tuj=KVKQfTX^rL{&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&C+P34GDC9*7#~l+%y^E&3?yx&SCQ8U7ICU4WVkxZf zVrQOG*ndF*jw_1Rp5me3bN2+cg-!Kh3m>RTTkoBdI{$+d$dq$Md_k)A&PkzAe=O!R znL6j0o)a#1utRd)r7enRN(GTzAP?O|(DTMh@T9SpGRwVYGh%XG1QxWi`IT%)6gTxH z;Mh=+GY*YHj)l<<0@h-vgaJwz(lnB5B(7!O)G*3LEEsLc0ZD8D2kQj&68Z#wRw zQWsFWYE_MGy*|Y{zrl7Y`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04v~$;Tv3eY{ZMDn6hYIB+$zl+; z`nsv$^AN^EqxGuvSo?;tg)3DX%cBZkB6uM}1%`+kjm8i7mcAd1;rE4c1Mf@tJ$=5v UyKbxf=gt)3`0U!c#$TTO0&rZlL;wH) literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-5-4.glb b/Resources/Tests/SharedImages/plane-5-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..f85acea0917ecd50f4d2b422fa66a50c4b4bee9b GIT binary patch literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&uHoukqg&5B6sN>lk%Fe>hfRiE zhA(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ^LShl01kd+y1cXcGxDw_3squvzhD8*0U)LFueW-v3C zJN=Zx`U?thTvNDviibhp*%MfXsmoy+=7FlR)!I3!{Xa;7Ogfju7o>9SoD>T6$6~&a zsePX5IpJ~#E4WZy+MJT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2<_SnzzH7=3 z;bJIUA!n7Z#%l$s8(3}C_zJ;F9!fC8()s~!=!fA1zOS?!7_Z@b`uqO=x-HjV+LMUm Ly=(IZf4TPyONg`9 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-5-6.glb b/Resources/Tests/SharedImages/plane-5-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..c33df9153a2f8959d33c6428253e2f61bb4d82bd GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Smq3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdPzu*RmZQ1+2{AxThnbcF}y$ANOZSM=5>;r_KsiG=r77 z+Uchh)?ZM7zgr+63+oIQbMn7VvT!#q$`w%R)(-2Zvv(it%?tB`R>5C!lQk zd6U5x0nCR&t5xN(@eOkeSL!}iM>W1eu#yK73=y?jKj2&Xel&&O7upTHFX8v}`Tp*@ SE%%>0lZfN9YwHGodG-sA<+XVL literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-5-7.glb b/Resources/Tests/SharedImages/plane-5-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..b5542077807f81832e30683d6bfee9261830caf6 GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|I5FzBp-`-K!9~qh?NSgalQ@Fho0s5GIklvXj z>*uzmG!UITGv}WBac;tResg6Q#?M!V@fP0OnK#)Y5(G5QxyX^b*do7Juc^3a^c!-q ziTp=Nz4bb$Qkwx!GM>^@&L1<1+>YIHOcd}Uh2yS?q~1ldes9#9A`>O#0h~HZSg{mV zc)2r=DeS+X0LK+Y`-$Ry(085)Yzv#}#TMREmA2kHC2jo=QXo^#74aFV+B+qMLjAFr z&t%#<&GeLTxq}^&>n?3kNK-0^Lg<2LyI|x{dp%MltVMNnVM##0>STXLka;Tk4x>1=wfua18h)_w&X}{?@ z`%0Zb?W$EZw*C4T+xiW*Tgmrdur>yw!=Rm$pj#?`mXMH^sTYDl*(80Cg`n8V(opq` zCA1ur@@i9UD_}w-6^z6sHuJ@pN)pE`nDQcwQW!_%!kmzt_Ib?34GDBN7q!r|vg&)%yThSGA zr6{NlpXO1OcP3cEB->Obky0?fkjmVmJ8w3e54{Np#zYfuI_`~-3x%8yKKDIu`r!)( z1pqSy%-|cOyI+^4G;}oWoz`~>Mp>`~`1QFIB+X+YwP+XD)-_g-V|J`gIrvbaoFrKc z!d7256?`7Rcxbdx$Yy6i&DFJ(l?$VlFB+e~Ps(xTx8xT?!&)5+})&BoihRyNd{YfIea$re`LJ z_ian5kT_>%zVn^ScM>IwyBkf@e!kJPHaz!pZ@NV|2uM*dULbe5MSdBEgg;X94Y{U{ z{AWSDFf52rX28;%Wh4`e=bRw7W7&?50#;^l+|`j#yJ$Y>kNY#EqZB`ZQ)dM$n!(Ck z?etR$>n|w4aZSgd_gMr&PkzAe=Oz; zncC->o)fNiu!9TLr7em`MmQFXBNyFA(DU1nv-CC=A}`!lE21JUeFIwA`ieI=N?Pg? zaBK)KD1$~J+c41%0#;(Eg#k(!lPnT3a;>&084p@H*3LEE$jtX(D8Ilwl9F=TZ@bQ+ zQWsFGZdHSA_fE0)Z?N53zW;)?OdvWA+Bpl_mif~ZN2E%<cavDHU{6n^*`={ZdGCiyplBXfg7ppcoxZz1gHcMlMuxG5kF6yxE5@CP)CN zAz}(2A=Un-HkF~mY44Q4Gcc-(rNFPw1;<&DV4+mI>=`}1V|1#ShvF1C%@feH{IJQe zivT7=;nb?~)p&)8g)4OjtD_oUAy~-+35HmHz&G^$XbSIF+6_Ff;eGo1{_eUh_g^}b Nh~u+s>jr;$_6ug;vuTFD6!tpy9!0;6V>=fb{~1}9LP{EUNmd3S4B1g^4cjuZoVJ9(9$*i-N7<2N z=iY6M8Jy@y`p$PQ-*J@8Zm$f(`1#H-Zs5I}`r{44VMvOC@dA1C4GPNDittBDo{(po zD0mUXU#$uvlo_%#XBo-F>?J42Yddb!L?J6PIPRE8s9iMe_eQ-5GEs`3!D({=E1JQ| zUTn={3g<5)uj!JNJcmoj3W=-MbPu>6=&&nEJR*-jYdR8Tm}}jvhfYCag;RF zCE!>SUQh;&Lat?_Ed;E@PzeK+Fd|tbV&pk3TQcsoa;Tk4x{;ahz)*gHc_by(Y;`-$ zeWlKzPSvU!+qyZ%y1&78D*65k*0F);FzDt<(5}p%rZ^%x^^#L4n;_4kG2`i&GgKq+FxSX`=zaP;fE1Rj3=%S>OR_|s z;3ZMvlOnG2&N)q~pzF%SLU0@FMZ#ieEe#I z0)QC;rtl3?-LFfd4HZpmtMr|Lp$nD*zd9EjXGMaA679Teb!~VYvu(BY+(Us1oaPDW zT7KMQ>_rIEp}=aT9oDX4YT-)NMSWD^HG-8qlwb(x2Yg3A49D>M#<+s_E&Luo-``)h S<^F4D5^;ETXuTFD6!tpy9znm2<4YDcGZ4Qp zeWlKzPTi^o+j(=0b$^5H*7E%qtYZVwVbI=5(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aiqFAZ-id|%N8_u zW|~^!Xuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_CT}%ry$`dY?WIAcZBX!jw$uk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&61=Hbd7>q$MCK?BmQSTP{P{`TfOFsxEAHUk5 z0APlIDSU%e_nXpaLq*fsDt+f*=z?Xyug(R>d6{CNL_3$sb*-)~pN<}Us8ETsA_ZZ~ zubT|Mj9@$zTCKFl#y5;DT&dfrk7~R|u!=_#3=y?jtsn3${V*KE?+fD!-k0!u{Ct0Z T*_Qjyok_&u*`;-ZKR^2gZbr32 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-1.glb b/Resources/Tests/SharedImages/plane-6-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..2c03127adf883478932512a62de7c3901aaffed3 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYrES)>F2Yh|l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(l`byKZpRY9S9lSSlf4V_93`tosULx;)gMuoG2*0D`0ePm5 zf+s=zC@P6iX2`OFn7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=?VNgj&?d6sKR#=TY!wR1@~GV>i6$}cdFrKBA9ZO_?P z>I`Z%t!lCEn`5l~8*HzU@4sLz6NnCjc20t}W&Sk7F{x9pIEAta@+c}!EThO{*)y7v zdQj@CUAZlR85V?7oYvSP;1eQnn$mE_syNAE9FYfef=e=BDdX2TRNcI9h2H$j7PNO3 zHMPXid_`dH%qyPj0(DLOe_SaK_#TQLAU;Vv>5qQP>hbI{%q18BM&OM7=9V}{_Nvd6C?oC z5HW?1kZONho7zy}xLYOg9E`eR8Stxf!Es)uSSZy(c`UtW^i27<_2ff|N}LrbC|iEs zWbkDO^P$jcReNlG!`#A^rjPYegRc>+;-LgXM4iqL_?CVcP2u;2b_MTC_&t8UzrSqD R{pZdk;_&R!y2YQL{Q`!~wQ&Fd literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-2.glb b/Resources/Tests/SharedImages/plane-6-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..d41d5b589e5caf6801b9d2dab35c045befd86181 GIT binary patch literal 1508 zcmb_c?P}XF6!kjx9znm2BimWn%wS|$3Moz8C0QASFl0xuHEhesa@rCCdw@OW9%NUt zo$qdA%-}><(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQSc?{tsB?`&YNSb^BZipmhZn{Z45+*LAxhGJ1T#cl89ERmx4jrBz=^npxDZ@NcD`R zv>KG^YFlk9U`iwvj3gB{55$N{k|Zpg@G^=s7)Rv6oREU{dBVjt33WH;jnM0V)q>{E ztfE#pmMtmF9o_;_?YHru1dBvN((HmexJ!~eCL_Ltxkmn7@6+c2q_E^=kkBbx(k1ew zD5(yg7IB?-E?CMWTh}I$Qm~+u%G{tke>$8E{V@o}L}Pz4>fIs_3OO5m>HGfV<5vs{ z0A>i7!8b^EzbQ>+=x91yt?vwss$ePb>vJhcRwP7f(as&qF}qe5tEW>9K2)e6X`X7jjm$MgRZ+ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-3.glb b/Resources/Tests/SharedImages/plane-6-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..91e683543dcf883c76eb7327245c6c0413489bf9 GIT binary patch literal 1512 zcmb_c+m6#P5Dkm?kCmUJG&d^93zX88x@w!HEi0s|LQd)=MoAnwPIp&TB|d-;5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^M3q372TXW4ZkL{WO3PE18o1qQUT^9`?YoOaYD z;8+u0QU;Aewqc?z1gyl+2m_QbB6%zl*WzT3v z>OrZmcI37MW>^qTaav=GfRBm5X-dN>tKuYwaYP=>2`)*WrHtR;P<35b>7eU>EMpP_SQVsXMptBoya*yD zDt=NXP2vTo85MNhm{L|g5$hQu~4dAIEG`&$JV<>SFPN7{-IDM&WaTD zEkANHg))RGQ9jhF_S-VR^um>0lZeB!OY0VYe)bEO4z^za literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-4.glb b/Resources/Tests/SharedImages/plane-6-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..7336fd6535a6ed885281ababd7c3292d5e9c42a0 GIT binary patch literal 1508 zcmb_c?P}XF6!kjx9znm2BRgK$%wS|$3Moz8C0QASFl0xuHEhesa@rCCdw@OW9%NUt zo$qdA%-}><(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQScuPLb3zK*=Lr|rB-GuUH$t!fRSTLs zvx-{bShl1vcX$g#wco~r5-bu8NwW*?;4Vq>n2h)m<{J5Ty-%M9kiwFeK|-f&Nteiz zqNF-}TEunUxnL=iY+ai~O2L9sDszMG{ONEu^v56=6OH}JsCSDzDCBJLrSJQbk6$q; z0GJ_Q2Hzmv{iZaPp`&rPTHhHMRl!o=*XL4@tVoE|qMf^zYj&+JR*zc^K2)e6X`X7j|Z~N&o-= literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-5.glb b/Resources/Tests/SharedImages/plane-6-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..5283d5b7c43f57378feb3665a551e40b91ae0bf0 GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Smq3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdPzu*RmZQ1+2{AxThnbcF}y$ANOZSM=5>;r_KsiG=r77 z+Uchh)?ZM7zgr+63+oIQbMn7VvT!#q$`w%R)(-2Zvv(it%?5C!lQk zd6U5x0nCR&t5xN(@eOkeSL!}iM>W1eu#yK73=y?jKj2&Xel&&O7upTHFX8v}`Tp*@ SE%%>0lZfN9YwHGodG-sA<+XVL literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-6.glb b/Resources/Tests/SharedImages/plane-6-6.glb new file mode 100644 index 0000000000000000000000000000000000000000..ac18670568591b3027f4cdbd632c0b9b00560afe GIT binary patch literal 1512 zcmb_c>u%F96b?hYN9Es9noFr5f1s33>a;eaEfZ2zAt!Ycqa=3x6@k&^h>{?2zUpB<;O>np=Be!en{_we3KgYgF8C?aLac!~V^28GpXMfg1>56HJo z6g~+OtX3rv%8Xc6u$<&#_EZq$zjj>DL=mfUSnirgs97}a_ilR=WTFf|f>Un+Bbvj= zUTn={3g<5e2?qBqtmT#*vS1V(58m#aY%$geXeC-Hxe9s?dT~w!h&uj?=c< z1RQI^OUj^8$hB;=g@Bb9YGHs9Zb=@C1o=+KmW+F?9BSv1Ze->=FqB_l9!p7iowr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8IFlgz2X$gCdi|xII*oFk7dtj zM)X0^TibG40y8WKr#RKvBIF|?aGKI+!m2pQ;W#26&Iv9_pQVgn<4ARL-Uz+^%N8_y zW|~^!Xuc$H?(8iP)qWcfQm}|OB+V?igS|K{5wWq>fE1Rj3R5zrOR_}1 z;1yBflQOCEE;!AopzGSiLU0;ZLYf(T?l+~;hKi=MRr=1s&;`qYU#$y{^D@OkiFV$#y0&~e=6)ov1Q~LPvmym~ z%R5fl5gZcbqm(1Z#%lztcqG9PSL+Adryqu6_`Wc%;C%_-u%F96b?hY$K>Bpnj0PD50uhLo7!fyWkRYd*WzT3v z>OrZmcI37MW>^qTaav=GfRBj4X-dNhtKuYwaYP=>2`)*WrHo(WP<35b>5|Zx`<3v`FxXEn%*af7ko;c>pOaSrw#YN|$7bya*yD zDt=NXP2vTo85MNZm{gJN_v_9vs>5P49^+2Bjx_a`5}njit7 zhKMPAgjD<6+SG;$$K5D_=U~(o%Ya{<3y$+L#X_leeq~&l@^SR8(N!zAo_{D*iL)XF zeanxWOrZ>6N|X<^s{OVMFuic4>1KV@;A;e{cqqXT*AMufez+OK?+fh$-k0!u_ephmW6 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-8.glb b/Resources/Tests/SharedImages/plane-6-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..2a1b3ae5e9acf0b6794054820f6ae5710761b8dd GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|m|Mk$Lb28b7d2b8OF^Ve;+RZHGGQ{YyNJ*S=wtRldS;S% z-?o$nqH|{EJKwo{Cs8uLz0ox7=PONn3-8^`pKK8hLsArs7sy*|QBbbegg;R74SA-H zf+s=z^|~NJnITJamXS=%pK^k{*Ou+*C}d>@#~mFBwTou`-l#W4I!f^)ICYk=q8Y5r zOJq9M=@?p5kH9clHF9Ve0ZV4f8-%*=p~c)czl&Kqj3_;tNu_cTNh0`eQMl z$<#j2^qg?HgB@I`E^SdnGQzQ79C_$2f}Xe5oTaT;h`jLH?TCuF3=C*x`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`M6yW4$g^BiG9I*YtetDRk(uwoP=0}VBqimzZ#vGQ zQWsFGZdHSIyQf(DH`q=s-+#ecCJ-G5?VJT|%lv7IBT}VaatdV=Y+j3h1Q!EIlIH|CCz{f=3B%$Gyl~J6*I3f?`1Q(>w62@JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob%N-}k2`)1xtZnoePe$BEdq5cF{GuddKLP@^P!dhYA%q%@Yu| z{JP2Dix9>`q1CGL*!YIAg)4O%tD_oUAy~;n35JMTt=13tmcAcO;P-`g1Mf@tJ$=5v UyKc+<=guVJ`0U!c!C#*J0)B?IQUCw| literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-6-9.glb b/Resources/Tests/SharedImages/plane-6-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..975cf35dcce6f3b63f8a644e39a22f963942f6b5 GIT binary patch literal 1512 zcmb_c>yFYu6s~K04>G@-(hCylA4OynHb7UvjR_%UTBhxow$n_fxEn&^1NfjmhG(WN z_f=zTQkXL{-}%nvJB^aX?Tw~sKVNCuJ9zKr-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?Sk6)HNHZyk_QqDVYPn1_w@Z}3coM38+c#B@9FdX U?R8u3KX)b($7k2p4gT`%7pFx$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7jhi0jcZdrPBr;Zq5`LR0?L-3 zHyL~pzuTFD6!tpy9znm2<4cxqW-ziWg_b1llB^6$7_y_-8n$I*Ic*7nJ-{Aw53(c4 z&b`|hGdR(a^qucqzT-HZU0oQ4@$;2oyo2{<8jRNnM-eGY#!KYiuTfYnmxSL@@_>BX zMB$Sl!E#v=q0ES71Zes9>DAQNTy5uAE+SkW9- z_IzU=QaFD>0gh{m&X(eC(D$|kj%AzjwJm!`rERr$OzQp*QXrGgCGiQV+&dQiT?@vi%LOaGbW) zCE!>QUQz~)Lat?_4Fs&jPzwW;FeG^_668A_TQcsnvag+Ux{;Z0!BBpIc`PO6b>4Qp zU8PQ-PTi^o+j(<{b$^5H*7E%qtYZVwe$d`g(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aHP6<-w3__mn~@S z%rv#c(R@K*?(7W^)qWcfQm}|OB+V|kgS$8_5Avg^yAtGCmG0`}fjCw=lLm_8_Fa02xeEe#I z0)QC;rtl3?-ET^x4HZpiqx7AFp$nD)zd9Ej=Vgk85-n86F)yu4TRxs1e5g=~vmymy z%deXZzKmcz6k4sc$Hq5|EnKPFsE=yAMzD%U5)2WwTCE@OE&XskhTj*)1-vie_wf1t U{=6;spF5L?{j+oH27h|?3vpGoMF0Q* literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-7-2.glb b/Resources/Tests/SharedImages/plane-7-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..4277e4d44c9356deb4077a77ec817b0de31f6a7b GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7q(&R*T%IeAE%mpC{cmaJOO3P z&zlUs2w*-GTCFONjc=G+xKj7AI;!y%f|WdwV2G&I`T^h455o!kzR<4VeF?wE&-eG2 SZMpy4nM53(U0OHz^Rr)x*|l^4 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-7-3.glb b/Resources/Tests/SharedImages/plane-7-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..a16497fd90219fcb5389bcf970dcf4cd62ff4844 GIT binary patch literal 1512 zcmb_c+m6~W5KXu0e;_|cAs5QFd1)zSC00PTKv$Izs+?egt0azWhusxI>Id{g`xzZO z3HNQQs*wU7+h@*P&LmEzH&=#X{Cs5?@8P|j1fvbYQAC7bTp)k8L1D365`Is~1M+PX zg-?+J{}%Qn?(TlSu+wDsO8srx@jflN79#Al>x@01h@^~Yj9 zlc{@}=_%oS3p-foE^SauGQzQB9Qo)rhMqT;oTZJGl)3Pm&6vtn5n9m7<~O{?anjV6 zfMZR#pbQ#?T+2pV2w01u5(X$?K(bh_kngl?#kkkXp>{6mMrFPOL-{4&-FN{SCHV$@gEdjtxYIL3<}byDER0;+T}F7o0-bBzcqtC$^PmvFaI3 zNjWIx)u!53z!XcuDNagk9`YfPI7w(UW<|WpU>uPTbAkowvV`#)9O-V(YN1#Ess;6( zX-TbcG+PjuJ9`U6mEYQf5-j31Nxcj1;4V(`6&|t$%ry$`I-frGAcX}h!h}rdf-I0P zc|mmeM69a3b52t#>AEtpl$?fzROSZV1(W`?AB;dSCK?6fVP}ARDCD&Fr5gm}k6&$2 z05C(q6uv>a`*mqbLr2rvYJF#5lm$zHU!O~kGm&7aMLY8>&sL9XUR&3C<(1rW`N;^D^)YgqY7UlSivI&hPWC(;CuR^KZ4&E#udCT;rICY V{_e7^_Mbabh{Ll>>l%N4_6vbVwle?# literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-7-4.glb b/Resources/Tests/SharedImages/plane-7-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..df1f59836c150937f33d2530494a1f532a1bfb44 GIT binary patch literal 1508 zcmb_c?TVW~6t!FW9$|iq(P*=+6iS+AAxjdsNxBq5D5Dv5iaIim*db{d}cq@8G?id6NyoK|qRv@dCMv4f4w{B>bL|2jrSM z@}C6p!muDhnE^|4mXS=%pK^lS*H*{YQNYR!j=MS%Y8TD=y-{z9bd=&paB44MMKf5L z%dLJ)Vf_UKIIbz29mV~iZ|?{!!_?(#8s?s=ven)xY3F~C0-1C!iO)#o-YF>*>W{^I zCezMorl*9KG>4LcmH4wJ<;lBa%fTMy}3GygPPAntOBH1&V zl4?+@t8KY0fhiV*Q=C-T+~;E=aFWnq%E~CtU>uPPbAk)fX9?psI8fbOG(xZcWeb`+ z^NL#HXtp9Scjgv|YQK#K2}HahIoO4}ILTu?W-FL$@Fho0s5GIklvXj z>*uzmG!UITGv}WBac;tResg6Q#?M!V@fP0OnK#)Y5(G5QxyX^b*do7Juc^3a^c!-q ziTp=Nz4bb$Qkwx!GM>^@&L1<1+>YIHOcd}Uh2yS?q~1ldes9#9A`>O#0h~HZSg{mV zc)2r=DeS+X0LK+Y`-$Ry(085)Yzv#}#TMREmA2kHC2jo=QXo^#74aFV+B+qMLjAFr z&t%#<&GeLTxq}^&>n?3kNK-0^Lg<2LyI|x{dp%MltVMNnVM##0>STXLka;Tk4x>1=wfua18h)_w&X}{?@ z`%0Zb?W$EZw*C4T+xiW*Tgmrdur>yw!=Rm$pj#?`mXMH^sTYDl*(80Cg`n8V(opq` zCA1ur@@i9UD_}w-6^z6sHuJ@pN)pE`nDQcwQW!_%!kmzt_Ib?34GDBN7q!r|vg&)%yThSGA zr6{NlpXO1OcP3cEB->Obky0?fkjmVmJ8w3e54{Np#zYfuI_`~-3x%8yKKDIu`r!)( z1pqSy%-|cOyI+^4G;}oWoz`~>Mp>`~`1QFIB+X+YwP+U|t7BeU*H}GHIrvbaoFrKc z!d7256?`7Rcxbdu%F96b?hY$K>Bpnj0PD50uhLo7!fyWkRYd*WzT3v z>OrZmcI37MW>^qTaav=GfRBj4X-dNhtKuYwaYP=>2`)*WrHo(WP<35b>5|Zx`<3v`FxXEn%*af7ko;c>pOaSrw#YN|$7bya*yD zDt=NXP2vTo85MNZm{gJN_v_9vs>5P49^+2Bjx_a`5}njit7 zhKMPAgjD<6+SG;$$K5D_=U~(o%Ya{<3y$+L#X_le-Zi?Wd|ds?xKb;po_{D*iL)XF zeanxWOrZ>6N|X<^s{OVMFuic4>1KV@;A;e{cqqXT*AMufez+OK?+fh$-k0!u_epggu{ literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-7-7.glb b/Resources/Tests/SharedImages/plane-7-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..00de2f419649b4ce14431bdeecfca344225e6826 GIT binary patch literal 1512 zcmb_c?QYXB6b(bXN9Es9>NKT;{DD$BsZ-mGwoFJ>g`Ct)jFLEVoNiN9i3i|8c?_H&=#X{Cs5?@8P|jdgBcuK|u4IiyXQ04f2cCii&$iACQYp zkhjUWTJ#Tf>UP!BbLGl zFSh0}h5Z*4;JBh_?+wDsI6Y3qNG0-18Ih|fsX+$kv(>W{^I zCezkwrl*99EzFQycWHw{no>a|7sy4oA@sbl5OMCo8QQqgmF`E z0**BmIpfeM)Uq(zLcm%Kl`udFLz;#%LayD$igB-%L+xDBjmmrnhVn}yLM17u{kG%m zD|H67t5((6_M2mD>o?d=CEtI++8BrqgLY1WZmIlPLPA=mUI+$dlk`y*f?_L6L)9~u z(DI;^TbpWH0TUvrU?eWFnJ-3Ek~n6;gco6y!f`|{oD-7M9*?=WA%X7ZycT-(S1qXb z%t~s7W9gE@xx-r^s{Ga-lwgslN$Oc}2YX4JMP$U6aITSe*ZuUl4=F5p;m35!mUM|+ zDGI8?r+HN6oe7pO$<~!gq!i3Aq%t??&YKQq18)q1G11tYjJiYQLLq1UFFntjeEf<* z0l*9aGq{6v_v_M>hK{Da)%s4sC<~SVzh0Muq77R2`c2ABv}md zR(G7X12`nwN3KSt-RLv}xDtw9Hg$NWF;%fYW`}D(L4Br>V6}&Iud;ENVciC3+ Q&#fuM;o7BfjXz)e1&W8ZJOBUy literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-7-8.glb b/Resources/Tests/SharedImages/plane-7-8.glb new file mode 100644 index 0000000000000000000000000000000000000000..5d110b0a72afb7f166191f2500c13843254be4e4 GIT binary patch literal 1508 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 M@a)pM#h;)30$%U50RR91 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-8-2.glb b/Resources/Tests/SharedImages/plane-8-2.glb new file mode 100644 index 0000000000000000000000000000000000000000..996dcf6ea7039612203326b0d5c612b438d9a532 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE^NcryGFMfd8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lgaI@9` literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-8-3.glb b/Resources/Tests/SharedImages/plane-8-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..c01f821a031a193fb260cb7b123ad7b58de769ab GIT binary patch literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Qw~3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq678Qj-(b77eE$V&nLu(-2Zvv(it%?TezF}_RO5MlmsK!?aR`NiCA);362YgH4kEZbZLc4+YCH$T~-``!g S<^FSL5^;QXZQbB6&wc@nv9)&q literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-8-4.glb b/Resources/Tests/SharedImages/plane-8-4.glb new file mode 100644 index 0000000000000000000000000000000000000000..7c325c9d370634b6fb762edee6f31173813eac16 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE?mRayGFMfd8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lkV6)l) literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-8-5.glb b/Resources/Tests/SharedImages/plane-8-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..8916d5b2a57d2a1b5270e12acc065b34352fb112 GIT binary patch literal 1500 zcmb_c>x$Yy6i&DFJ(l?$VlFB+e~Ps(xTx8xT?!&)5+})&BoihRyNd{YfIea$re`LJ z_ian5kT_>%zVn^ScM>IwyBkf@e!kJPHaz!pZ@NV|2uM*dULbe5MSdBEgg;X94Y{U{ z{AWSDFf52rX28;%Wh4`e=bRw7W7&?50#;^l+|`j#yJ$Y>kNY#EqZB`ZQ)dM$n!(Ck z?etR$>n|w4aZSgd_gMr&PkzAe=Oz; zncC->o)fNiu!9TLr7em`MmQFXBNyFA(DU1nv-CC=A}`!lE21JUeFIwA`ieI=N?Pg? zaBK)KD1$~J+c41%0#;(Eg#k(!lPnT3a;>&084p@H*3LEE$jtX(D8Ilwl9F=TZ@bQ+ zQWsFGZdHSA_fE0)Z?N53zW;)?OdvWA+Bpl_mif~ZN2E%<cavDHU{6n^*`={ZdGCiyplBXfg7ppcoxZz1gHcMlMuxG5kF6yxE5@CP)CN zAz}(2A=Un-HkF~mY44Q4Gcc-(rNFPw1;<&DV4+mI>=+%rXY{I>hvF1C%@feH{IJQe zivT7=;nb?~)p&)8g)4OjtD_oUAy~-+35HmHz&G^$XbSIF+6_Ff;eGo1{_eUh_g^}b Nh~u+s>jr;$_6ugmvx$Yy6i&DFJ!F1|m|Mk$Lb28b7d2b8OF^Ve;+RZHGGQ{YyNJ*S=wtRldS;S% z-?o$nqH|{EJKwo{Cs8uLz0ox7=PONn3-8^`pKK8hLsArs7sy*|QBbbegg;R74SA-H zf+s=z^|~NJnITJamXS=%pK^k{*Ou+*C}d>@#~mFBwTou`-l#W4I!f^)ICYk=q8Y5r zOJq9M=@?p5kH9clHF9Ve0ZV4f8-%*=p~c)czl&Kqj3_;tNu_cTNh0`eQMl z$<#j2^qg?HgB@I`E^SdnGQzQ79C_$2f}Xe5oTaT;h`jLH?TCuF3=C*x`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`M6yW4$g^BiG9I*YtetDRk(uwoP=0}VBqimzZ#vGQ zQWsFGZdHSIyQf(DH`q=s-+#ecCJ-G5?VJT|%lv7IBT}VaatdV=Y+j3h1Q!EIlIH|CCz{f=3B%$Gyl~J6*I3f?`1Q(>w62@JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob%N-}k2`)1xtZnoePe$BEdq5cF{39rhHtzYjmr@hYA%q%@Yu| z{JP2Dix9>`q1CGL*!YIAg)4O%tD_oUAy~;n35JMTt=13tmcAcO;P-`g1Mf@tJ$=5v UyKc+<=guVJ`0U!c!C#*J0)9%hQUCw| literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-8-7.glb b/Resources/Tests/SharedImages/plane-8-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..1b60bb7f7c9f36e5d4e3b2037bc507b808074840 GIT binary patch literal 1508 zcmb_c>x$Dr6pkytN15Lt&82p05Tw>FbZMKlt&32KOwvg*l4QbUQgjTxi{S+90a5&7%z~!+##0_NJlAt0H@9hRy2c^ zx!UQ+6xLr*fa98?^F(n!95_z|mSO7hH4Sr5RoQCql+^woq(CN}OX4$9xpztmh5BPL zpUKod&GeLTwSyg8s4i_$L^8s$U>v#VHiDkFLeA1wEJR+o?RG>(T>1vIvi%iraFn#w zCE(Z)UQh;&LbhR|9R#ezPzwW;FeX_fV&qyKQ!?(ga;Tk4x{;YbfuZ~Y^GHg{>AdMW z`%0Zbt-4hWw$nSt+P}eeYx({Q)-r+UFlgr_Xj|q_Qyh^h^^#L4n;;LOJT!IQ2^*%`Lj~=A*^Pn}T9=H1%ea{usGX$;I&V!1HDwzL+2Z zpoWMke1uf{o7z-{3a7JE0?)vxDwYDjIu{&gMS_J=?ef~VHs#~!J)>7mK9s1yX`XyTXUj#583awU^$Hq6zEnKPlSRK{)3c*SqNH9dyYW;w3>HE(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ?_YuS#DLRMz5-O-Uyt7z8mje1k0qZB`ZQ)dY?n!(Im z?(|a%>n|w4aZTawDINxWXHQ@mrY?tRm^_WvLSGU;3rUy#bRb5bbOAB*`y zruKQJ=Y-20tl&a*X^SF~5sn4p$U}D#^t`p^EN#U?a7@t~Ds?OfB1%zO`q@(au(DJjQ&*KrP& zx`0}Bs~W7^J;mC;!FFo-{tMPJf#^7B=PYPj=1)@`kt+3)Qz)AtPom_+H1aHxJ)JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2<_SnzzH7=3 z;bJIUA!n7Z#%l$s8(3}C_zJ;F9!fC8()s~!=!fA1zOS?!7_Z@b`uqO=x-HjV+LMUm Ly=(IZf4TPyS3tAk literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-8-9.glb b/Resources/Tests/SharedImages/plane-8-9.glb new file mode 100644 index 0000000000000000000000000000000000000000..469cb3393075aa6e206313ef7b611c2e513e8faa GIT binary patch literal 1508 zcmb_c?TXq!6iv7EJ!F1|m_#cY3dLF%T-0pUE(MV?iIZeXk_nTEyNd{YfIem)q<1EX z-`kebKy>cRoO|xaxe4RN?Tw~sKi_EDJ9zKr?sSW=?~^=dJV(xQi@YKT2!EjD5jm!g zyk|k&AjpYOrq7a$r6d)L=Zqldt!1}#OJq99IW{^I zAyfN2({sYr4t8*^y0k?hNeRb-apa)85PIGSI7=Fl5Lxatn;{iZ;Th1%<~O{-Vcb-g zfMY{=P8l=`*@lUB5U>(MB@9r)n53bIkYlw?$#~Gpv39QMMrOVTL-_^fp_Ejs-Rrat zmAZghRjX=jyL*bYe}nB*^8FXAWdhN0(5_Ek5Rwa}}7*@F7c zqNJ8Mnyv}Vow);|%5UvK0uirCj&|WLjn3B*eHae~R;$us?Ha}wu2gL-k1BkLUx$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?ge|{r zGWH^X@las3svI`1VQk?_-Nx#u##abd@<4(ipjNB(1HPs2M^pHHq20jy5`Isg@9(bL Sa{swAi8wyHwr=p3XTJc9mbGF4 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-9-1.glb b/Resources/Tests/SharedImages/plane-9-1.glb new file mode 100644 index 0000000000000000000000000000000000000000..9ea21e3509d3cdea2abbf97aa592d9f5b01117fa GIT binary patch literal 1504 zcmb_c>uTFD6!tpy9!0;6<4YDA{~1}9LP`>MNmd3W4B1g^4cju3oVA3&9$*i-N7<2V z=iY6M8Jy@y`p$PQ-*KGIZ?6o)`1!^#Zs5I}1(OZJQAC7bTp<5(gTivPBK(1pZ^*Yz z6h2E5tX6_ZZAL6BSWa>|e=Z2}-#V^mqKK6_9Cu74^)8zAd!ybInJB|g;M7~disrDg z7hCg~!ubmda9mTgcN7nUzPBTAEZbDCZP|OO%GP_Qr0)M91v2Ga5ucH&y;D*s)E|ra zLZSyR<XIfeRGU;e}nDR^8FXAV*}A)(B4VVuF9WgI3`u zQVmLVwWYQdFvF5?iqi^PgnUdSPE#69Ss5oej3e@4POu<-mNI^eBi+r%M(FjwYC&^n zUQsI?&6fn`&fWr1?YHru1dDh>((HmexQo*w!DF_BxkkZ#@8hQdq_AXVn35S?k|pvb zFNqGHh@{TD;54I>u4@xZ$!S^B^||CY7b%unv``(#>{?wL9;cdnD3HKek%F$( z$4$j9BA5;h)~m{4;~J(GuGC$uj%s{`U?qx$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mQqP1oK literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-9-3.glb b/Resources/Tests/SharedImages/plane-9-3.glb new file mode 100644 index 0000000000000000000000000000000000000000..5f40c7b6e365db2f31169f8b1c45e43b392f27a6 GIT binary patch literal 1512 zcmb_c?T*qw6s>D~4>G@-GHsDa|0p7xumQRPZcGR<(=wDXZKs(|aW{m-2k=3C4DU=^ zzE_R0Nn!5HoO|xaxsBr4?Tuj=KVKQfJ9zJ={&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&CUfYgqqL3FU9Jfs*^)8zBy2I`SnJ6KT;M85filwl^ zi=BB&VgCgMIIbvKdy0pC&)pN)7BiiEX$}IPq&4|f$5m?a5=2x;IQQXv* zfMY{N&Nws*ITl7c2w01u5(X$?NYhBJk!QECVmxT&SUcBrqcY!vq5P7FNJ+|Vb=vNs zQWsFWYE_MGy*b4?zrnUE`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04v~$;Tv3eY{V|CQRE~g*Ll#?Wj zLEY*Tr!vSxm=WzluS&Ny0n9F3sd`x+RrnIY3lSOJq9M=@ip5kFR=qjven)>sr^4lflNA=#22J;@0=70^~Yj9 zlc{~4={ezQ2Rpb>UD~3EWQ1eEIC9Zl1U+wsoTaT;h`eyy?TCuF^bKfb`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`OtMJC$h90(G9I*YtetDRk(uwoP=0}VBqh~xdfm>U zQWsFGZdHSI-kf6X-(b77eE$V&nLuCe#u2$NC%7O3mN0&c1J%uCBlP-TwxGGQ zsHi25W@`d-XYPQg_S<-nK*Sr8qg}X*lRU-~wuZSz-hKb$ry-=UW~HByIbD-Aazlui zsQ5_{*NNwxrc}^PZDJuf^-CemExPySqs7RZf?{+u^=6a)7`af%#qjgM^JX8um>>b5 zhKMPAgjD;R+Ej)L$Jr@?XJAwnOMzdV3y!lQ!9uBa=@^dQGkPXGRyFugp#rCQ0>YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mUuC;0a literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-9-5.glb b/Resources/Tests/SharedImages/plane-9-5.glb new file mode 100644 index 0000000000000000000000000000000000000000..22909852b90854c08f146b388ed637922a7defdb GIT binary patch literal 1504 zcmb_c>uTFD6!tpy9!0;6V>=fb{~1}9LP{EUNmd3S4B1g^4cjuZoVJ9(9$*i-N7<2N z=iY6M8Jy@y`p$PQ-*J@8Zm$f(`1#H-Zs5I}`r{44VMvOC@dA1C4GPNDittBDo{(po zD0mUXU#$uvlo_%#XBo-F>?J42Yddb!L?J6PIPRE8s9iMe_eQ-5GEs`3!D({=E1JQ| zUTn={3g<5)uj!JNJcmoj3W=-MbPu>6=&&nEJR*-jYdR8Tm}}jvhfYCag;RF zCE!>SUQh;&Lat?_Ed;E@PzeK+Fd|tbV&pk3TQcsoa;Tk4x{;ahz)*gHc_by(Y;`-$ zeWlKzPSvU!+qyZ%y1&78D*65k*0F);FzDt<(5}p%rZ^%x^^#L4n;_4kG2`i&GgKq+FxSX`=zaP;fE1Rj3=%S>OR_|s z;3ZMvlOnG2&N)q~pzF%SLU0@FMZ#ieEe#I z0)QC;rtl3?-LFfd4HZpmtMr|Lp$nD*zd9EjXGMaA679Tgwau>8wc&B}+(Us1oaPDW zT7KMQ>_rIEp}=aT9oDX4YT-)NMSWD^HG-8qlwb(x2Yg3A49D>M#<+s_E&Luo-``)h S<^F4D5^;ETXyFYu6s~K04>G@-(hCylA4OynHb7UvjR_%UTBhxow$n_fxEn&^1NfjmhG(WN z_f=zTQkXL{-}%nvJB^aX?Tw~sKVNCuJ9zKr-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?z-gX< zyyZ7eW>5q$B+7+aRc;#s7+$ziH?umb@fCuVJdj`rtMvoEr|(Bo_!21$@PoM8^ UuiJ9}xig74KD)MV@Rw)50H-0gS^xk5 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/plane-9-7.glb b/Resources/Tests/SharedImages/plane-9-7.glb new file mode 100644 index 0000000000000000000000000000000000000000..77b088bfe57108c448c7760fed648c1872a09bc6 GIT binary patch literal 1512 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 literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/texture1.png b/Resources/Tests/SharedImages/texture1.png new file mode 100644 index 0000000000000000000000000000000000000000..9594525097353c3fefa248b0cc9732700e301425 GIT binary patch literal 57706 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 Y$@7zpj^cR82v(Y@vBf8uMvkGs0ipHazyJUM literal 0 HcmV?d00001 diff --git a/Resources/Tests/SharedImages/tileset.json b/Resources/Tests/SharedImages/tileset.json new file mode 100644 index 000000000..b7569c61b --- /dev/null +++ b/Resources/Tests/SharedImages/tileset.json @@ -0,0 +1,2228 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4096, + "root": { + "boundingVolume": { + "box": [ + 5.4500005, + 0, + 5.4500005, + 5.450000286102295, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 5.450000286102295 + ] + }, + "geometricError": 1024, + "children": [ + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 0.5, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-0-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 1.6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-1-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 2.7, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-2-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 3.8000001999999995, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-3-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 4.9, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-4-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 6, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-5-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 7.1000004, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-6-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 8.2000003, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-7-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 9.3, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-8-9.glb" + } + }, + { + "boundingVolume": { + "box": [ + 0.5, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-0.glb" + } + }, + { + "boundingVolume": { + "box": [ + 1.6, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-1.glb" + } + }, + { + "boundingVolume": { + "box": [ + 2.7, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-2.glb" + } + }, + { + "boundingVolume": { + "box": [ + 3.8000001999999995, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-3.glb" + } + }, + { + "boundingVolume": { + "box": [ + 4.9, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-4.glb" + } + }, + { + "boundingVolume": { + "box": [ + 6, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-5.glb" + } + }, + { + "boundingVolume": { + "box": [ + 7.1000004, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-6.glb" + } + }, + { + "boundingVolume": { + "box": [ + 8.2000003, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-7.glb" + } + }, + { + "boundingVolume": { + "box": [ + 9.3, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-8.glb" + } + }, + { + "boundingVolume": { + "box": [ + 10.400001, + 0, + 10.400001, + 0.5, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0.5 + ] + }, + "geometricError": 512, + "content": { + "uri": "plane-9-9.glb" + } + } + ], + "refine": "ADD" + } +} \ No newline at end of file diff --git a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp index d40159b08..92a5bf922 100644 --- a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp @@ -10,6 +10,7 @@ #include "CesiumTestHelpers.h" #include "Engine/World.h" #include "GlobeAwareDefaultPawn.h" +#include "Interfaces/IPluginManager.h" #include "Misc/AutomationTest.h" #include "Tests/AutomationCommon.h" #include "Tests/AutomationTestSettings.h" @@ -26,6 +27,16 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter); static void setupForSharedImages(SceneGenerationContext& context) { + static FString Path = + IPluginManager::Get().FindPlugin(TEXT("CesiumForUnreal"))->GetBaseDir(); + // IPluginManager returns a relative path by default - convert it to an + // absolute path. + const FString FullPluginsPath = + IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*Path); + static FString TilesetPath = + TEXT("file://") / FullPluginsPath / + TEXT("Resources/Tests/SharedImages/tileset.json"); + context.setCommonProperties( FVector(21.16677692, -67.38013505, -6375355.1944), FVector(-12, -1300, -5), @@ -46,14 +57,7 @@ static void setupForSharedImages(SceneGenerationContext& context) { ACesium3DTileset* tileset = context.world->SpawnActor(); tileset->SetTilesetSource(ETilesetSource::FromUrl); - // Unreal returns the relative path of the plugins directory by default - FString FullPluginsPath = - IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead( - *FPaths::ProjectPluginsDir()); - tileset->SetUrl(FString::Printf( - TEXT( - "file://%scesium-unreal/extern/cesium-native/Cesium3DTilesSelection/test/data/SharedImages/tileset.json"), - *FullPluginsPath)); + tileset->SetUrl(TilesetPath); tileset->SetActorLabel(TEXT("SharedImages")); tileset->SetGeoreference(georeference); diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp b/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp index 01fda781b..34dcdf0a0 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumTestHelpers.cpp @@ -6,6 +6,7 @@ #if WITH_EDITOR #include "Editor/EditorPerformanceSettings.h" +#include "Interfaces/IPluginManager.h" #endif namespace CesiumTestHelpers { From 8020401328e6cd311d59184d0e41f9658751758f Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Oct 2024 13:18:53 +1000 Subject: [PATCH 12/23] Const correctness, doc. --- .../Private/CesiumGltfTextures.cpp | 9 +++++---- .../Private/ExtensionImageCesiumUnreal.cpp | 7 ++++--- .../Private/ExtensionImageCesiumUnreal.h | 20 +++++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index a9c8b20fd..a6d93f014 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -16,7 +16,7 @@ using namespace CesiumGltf; namespace { // Determines if a glTF primitive is usable for our purposes. -bool isValidPrimitive(Model& gltf, MeshPrimitive& primitive); +bool isValidPrimitive(const Model& gltf, const MeshPrimitive& primitive); // Determines if an Accessor's componentType is valid for an index buffer. bool isSupportedIndexComponentType(int32_t componentType); @@ -169,8 +169,8 @@ bool isSupportedPrimitiveMode(int32_t primitiveMode) { // Determines if a glTF primitive is usable for our purposes. bool isValidPrimitive( - CesiumGltf::Model& gltf, - CesiumGltf::MeshPrimitive& primitive) { + const CesiumGltf::Model& gltf, + const CesiumGltf::MeshPrimitive& primitive) { if (!isSupportedPrimitiveMode(primitive.mode)) { // This primitive's mode is not supported. return false; @@ -190,7 +190,8 @@ bool isValidPrimitive( return false; } - Accessor* pIndexAccessor = Model::getSafe(&gltf.accessors, primitive.indices); + const Accessor* pIndexAccessor = + Model::getSafe(&gltf.accessors, primitive.indices); if (pIndexAccessor && !isSupportedIndexComponentType(pIndexAccessor->componentType)) { // This primitive's indices are not a supported type, so the primitive is diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp index bb0df6f8d..f1081af7e 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp @@ -75,9 +75,10 @@ ExtensionImageCesiumUnreal::getFuture() const { namespace { -// Returns a Future that will resolve when the image is loaded. It _may_ also -// return a Promise, in which case the calling thread is responsible for doing -// the loading and should resolve the Promise when it's done. +// Returns the ExtensionImageCesiumUnreal, which is created if it does not +// already exist. It _may_ also return a Promise, in which case the calling +// thread is responsible for doing the loading and should resolve the Promise +// when it's done. std::pair>> getOrCreateImageFuture( const AsyncSystem& asyncSystem, diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h index a9aeeda21..133361c94 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h +++ b/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h @@ -58,10 +58,30 @@ struct ExtensionImageCesiumUnreal { bool needsMipMaps, const std::optional& overridePixelFormat); + /** + * Constructs a new instance. + * + * @param future The future that will resolve when loading of the + * {@link getTextureResource} is complete. + */ ExtensionImageCesiumUnreal(const CesiumAsync::SharedFuture& future); + /** + * Gets the created texture resource. This resource should not be accessed or + * used before the future returned by {@link getFuture} resolves. + */ const TSharedPtr& getTextureResource() const; + + /** + * Gets the future that will resolve when loading of the + * {@link getTextureResource} is complete. This future will not reject. + */ CesiumAsync::SharedFuture& getFuture(); + + /** + * Gets the future that will resolve when loading of the + * {@link getTextureResource} is complete. This future will not reject. + */ const CesiumAsync::SharedFuture& getFuture() const; private: From b1b036855de657e74934b8daca0ce94c859de4dc Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Oct 2024 15:26:52 +1000 Subject: [PATCH 13/23] Cleanup, make some useful warnings visible. --- .../CesiumRuntime/Private/CesiumGltfComponent.cpp | 13 +++++-------- Source/CesiumRuntime/Private/CesiumGltfComponent.h | 1 - Source/CesiumRuntime/Private/CesiumGltfTextures.h | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index cc535c220..931249e0d 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -1766,9 +1766,10 @@ static void loadIndexedPrimitive( } else { UE_LOG( LogCesium, - VeryVerbose, + Warning, TEXT( - "Skip loading primitive due to invalid component type in its index accessor.")); + "Ignoring a glTF primitive because the componentType (%d) of its indices is not supported."), + indexAccessorGltf.componentType); } } @@ -2132,8 +2133,8 @@ void applyGltfUpAxisTransform(const Model& model, glm::dmat4x4& rootTransform) { } else { UE_LOG( LogCesium, - VeryVerbose, - TEXT("Unknown gltfUpAxis value: {}"), + Warning, + TEXT("Ignoring unknown gltfUpAxis value: {}"), gltfUpAxisValue); } } @@ -3258,10 +3259,6 @@ UCesiumGltfComponent::UCesiumGltfComponent() : USceneComponent() { PrimaryComponentTick.bCanEverTick = false; } -UCesiumGltfComponent::~UCesiumGltfComponent() { - UE_LOG(LogCesium, VeryVerbose, TEXT("~UCesiumGltfComponent")); -} - void UCesiumGltfComponent::UpdateTransformFromCesium( const glm::dmat4& cesiumToUnrealTransform) { for (USceneComponent* pSceneComponent : this->GetAttachChildren()) { diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.h b/Source/CesiumRuntime/Private/CesiumGltfComponent.h index a55d8b57c..e78b2d273 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.h +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.h @@ -91,7 +91,6 @@ class UCesiumGltfComponent : public USceneComponent { bool createNavCollision); UCesiumGltfComponent(); - virtual ~UCesiumGltfComponent(); UPROPERTY(EditAnywhere, Category = "Cesium") UMaterialInterface* BaseMaterial = nullptr; diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.h b/Source/CesiumRuntime/Private/CesiumGltfTextures.h index 7f0326ad1..d3a7544d1 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.h +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.h @@ -15,8 +15,8 @@ struct Model; class CesiumGltfTextures { public: /** - * Creates all of the textures that are required by the given glTF, and adds - * `ExtensionUnrealTexture` to each. + * Creates all of the texture resources that are required by the given glTF, + * and adds `ExtensionImageCesiumUnreal` to each. */ static CesiumAsync::Future createInWorkerThread( const CesiumAsync::AsyncSystem& asyncSystem, From d9dffcd20bfad814aee9ba645f4e3fc46a53a673 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 3 Oct 2024 15:27:06 +1000 Subject: [PATCH 14/23] Fix metadata crash caused by failed move from const. --- Source/CesiumRuntime/Private/CesiumGltfComponent.cpp | 4 ++-- Source/CesiumRuntime/Private/CreateGltfOptions.h | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp index 931249e0d..ea7d9821a 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfComponent.cpp @@ -2242,8 +2242,8 @@ loadModelAnyThreadPart( return CesiumGltfTextures::createInWorkerThread(asyncSystem, *options.pModel) .thenInWorkerThread( - [transform, ellipsoid, options = std::move(options)]() - -> UCesiumGltfComponent::CreateOffGameThreadResult { + [transform, ellipsoid, options = std::move(options)]() mutable + -> UCesiumGltfComponent::CreateOffGameThreadResult { auto pHalf = MakeUnique(); loadModelMetadata(pHalf->loadModelResult, options); diff --git a/Source/CesiumRuntime/Private/CreateGltfOptions.h b/Source/CesiumRuntime/Private/CreateGltfOptions.h index 57923497a..c809652e7 100644 --- a/Source/CesiumRuntime/Private/CreateGltfOptions.h +++ b/Source/CesiumRuntime/Private/CreateGltfOptions.h @@ -41,6 +41,10 @@ struct CreateModelOptions { tileLoadResult(std::move(other.tileLoadResult)) { pModel = std::get_if(&this->tileLoadResult.contentKind); } + + CreateModelOptions(const CreateModelOptions&) = delete; + CreateModelOptions& operator=(const CreateModelOptions&) = delete; + CreateModelOptions& operator=(CreateModelOptions&&) = delete; }; struct CreateNodeOptions { From 6451ef3866780ff9681e788ca5003e8e07f97510 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Thu, 3 Oct 2024 15:54:48 -0400 Subject: [PATCH 15/23] Use ion for shared images test --- Resources/Tests/SharedImages/plane-0-0.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-0-1.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-0-2.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-0-3.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-0-4.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-0-5.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-0-6.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-0-7.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-0-8.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-0-9.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-1-0.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-1-1.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-1-2.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-1-3.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-1-4.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-1-5.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-1-6.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-1-7.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-1-8.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-1-9.glb | Bin 1504 -> 0 bytes Resources/Tests/SharedImages/plane-2-0.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-2-1.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-2-2.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-2-3.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-2-4.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-2-5.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-2-6.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-2-7.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-2-8.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-2-9.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-0.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-1.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-2.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-3.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-3-4.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-5.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-6.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-3-7.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-3-8.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-3-9.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-4-0.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-4-1.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-4-2.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-4-3.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-4-4.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-4-5.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-4-6.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-4-7.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-4-8.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-4-9.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-5-0.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-5-1.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-5-2.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-5-3.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-5-4.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-5-5.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-5-6.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-5-7.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-5-8.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-5-9.glb | Bin 1504 -> 0 bytes Resources/Tests/SharedImages/plane-6-0.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-6-1.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-6-2.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-6-3.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-6-4.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-6-5.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-6-6.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-6-7.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-6-8.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-6-9.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-7-0.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-7-1.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-7-2.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-7-3.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-7-4.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-7-5.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-7-6.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-7-7.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-7-8.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-7-9.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-8-0.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-8-1.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-8-2.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-8-3.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-8-4.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-8-5.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-8-6.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-8-7.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-8-8.glb | Bin 1500 -> 0 bytes Resources/Tests/SharedImages/plane-8-9.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-9-0.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-9-1.glb | Bin 1504 -> 0 bytes Resources/Tests/SharedImages/plane-9-2.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-9-3.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-9-4.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-9-5.glb | Bin 1504 -> 0 bytes Resources/Tests/SharedImages/plane-9-6.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-9-7.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/plane-9-8.glb | Bin 1508 -> 0 bytes Resources/Tests/SharedImages/plane-9-9.glb | Bin 1512 -> 0 bytes Resources/Tests/SharedImages/texture0.png | Bin 58902 -> 0 bytes Resources/Tests/SharedImages/texture1.png | Bin 57706 -> 0 bytes Resources/Tests/SharedImages/tileset.json | 2228 ----------------- .../Private/Tests/Cesium3DTileset.spec.cpp | 14 +- 104 files changed, 2 insertions(+), 2240 deletions(-) delete mode 100644 Resources/Tests/SharedImages/plane-0-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-0-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-1-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-2-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-3-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-4-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-5-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-6-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-7-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-8-9.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-0.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-1.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-2.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-3.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-4.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-5.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-6.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-7.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-8.glb delete mode 100644 Resources/Tests/SharedImages/plane-9-9.glb delete mode 100644 Resources/Tests/SharedImages/texture0.png delete mode 100644 Resources/Tests/SharedImages/texture1.png delete mode 100644 Resources/Tests/SharedImages/tileset.json diff --git a/Resources/Tests/SharedImages/plane-0-0.glb b/Resources/Tests/SharedImages/plane-0-0.glb deleted file mode 100644 index b3ac6b99d5b04e561dd1493cad7d5d7fae3e5415..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 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 diff --git a/Resources/Tests/SharedImages/plane-0-5.glb b/Resources/Tests/SharedImages/plane-0-5.glb deleted file mode 100644 index b81e52b3c9da06b1f6fc10699d0b4dab12b3084d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE=A8edjw@#p(R!O4GETZ#2z?bvyGX8-&ANYo#b`9h@j zX{4uw%PnYdsj9R=G06$Xf^p=b+Zbwo9dVXjCqfjZ*XhJmBvoKQDLdcr8pmlzT>_3Z z;U#5IC}bNZ+JeKX8Jfm`P8gFs776k!*OZL=UODWYOMN3F-+>|h0`pk5l;ghZIr~hV zL9M1#E!KT|jJ1D*?KSQDFIdY2qC=;h6Q^w%Kh1DV>d-4rA#H*@iHZ};DDqg=jAo=B zl=^B%swFVPf^dq{8e0VXjtHElG@P<3PI4GW;Vw>#1mCe0%r)}w`=34!A%qpHf|ShYimZ?qfyYGo zPs*eTyx=sWg03483&Ckn2`O&Sy+0exNB#slMn@BWdeCC)ei``w^y60(S^%iQ zV+xOuDu0`sT2St|Th-t>2zACX;8*8@k_}VDRM!yrCaP6ZpQ;u3)`}@A2>Z`^&P_zjP)Mhi8}e IE&lxM7gtxa`2YX_ diff --git a/Resources/Tests/SharedImages/plane-0-6.glb b/Resources/Tests/SharedImages/plane-0-6.glb deleted file mode 100644 index 0927e2f83f5a3700e80fa79ec3b52ec82328e3d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_cZHn745Oufo9znm2_Iw` z?0j!qN)5I(lHR=0%p1k&?E1z=-BB zvKL$Pn8NuB3UFLgbaoW?gTA*Ta4g%DuWi|TDs8K|Q&RVTkOG->E{V@b<=iPLBkTyXcMa7A26?rUc zMl+%tMQ?4(X$j1*Ae`bu_H zduEzi;%L4k(0BG0h-$x$2Ps&@8!I)?qOh&z1bW(T0kqvsL=e!O#iIfM2Z(j`K3bLWy?XwYs)^I;t<_WgvqsaaN=t zY#VN8aMd!wO?+kwL$;@ diff --git a/Resources/Tests/SharedImages/plane-0-7.glb b/Resources/Tests/SharedImages/plane-0-7.glb deleted file mode 100644 index 42f64e1c345b96d513ebfb5a46ff912d86822503..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$AjzCF^PTTpCraiwSDL2%e5Gmc;JuxBlMTW_K#GF#0=bI~^2;zJ{GO5r$U}Xl|T^$LfMYDcy)SDt5rT7tyI!jp53|8iH zs~=NXe?b9;Yl_Z};(pL~b_AAT>hd)Wb5B*tN_R?X{|{0ilg1_S8L8Brl0u^Xn9OG) zwNE2GC0uSng9}xq4T?xcI2Mc}7u`ls^H#`N+KPq93%A{lsEAA7fKs-<;WdtuwmJkH zYr+f4pisy*Otb}uRWsC$0i7@+StMfQS{+j|?tA61cP{mfjC==%^b5=**-}pDZP(dn z>I`btrE0L9H^*4}H`s37zW;)?OdvXR+BtFBmhsaRN2Ch9w62@Ar~?^AAIS1-t^;F6IuYM z!D9-SkSc$Zn@UjbbhfI&GZ3nbrNFQD1;<&DV4=Epacx|i@^RE)%G*FXTi`TLptI%m zrsM$TL!k?~sw_6PD@a|(YOBUq2v+hyg2AI!>j&JWABGe7zR<4VeF@*==llE1veZAf OClQBxm(~sbeD4>AHMMX6 diff --git a/Resources/Tests/SharedImages/plane-0-8.glb b/Resources/Tests/SharedImages/plane-0-8.glb deleted file mode 100644 index 307a4df5d40b7bc3026355f61227d9f19fc50ddb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_cTZ-E-6m_R`A3@*7@v9;APf61ZWJuy^k`9FsitH%1iftKL&dd-3T|k%E#q>(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ?_YuS#DLRMz5-O-Uyt7z8mje1k0qZB`ZQ)dY?n!(Im z?(|a%>n|w4aZTawDINxWXHQ@mrY?tRm^_WvLSGU;3rUy#bRb5bbOAB*`y zruKQJ=Y-20tl&a*X^SF~5sn4p$U}D#^t`p^EN#U?a7@t~Ds?OfB1%zO`q@(au(DJjQ&*KrP& zx`0}Bs~W7^J;mC;!FFo-{tMPJf#^7B=PYPj=1)@`kt+3)Qz)AtPom_+H1aHxJ)JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2`K~ED zgo~kYg`8Es8m|?kZeX=l<0}L!c__gUOX~-`p&y15_`cF^V7!L!>F@jd>$Y5fX-^`K L_pZ$w{N>&+N5Hev diff --git a/Resources/Tests/SharedImages/plane-0-9.glb b/Resources/Tests/SharedImages/plane-0-9.glb deleted file mode 100644 index e351c781242929e76e0185d40602d68e0c1697dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x!E|6t-LX9$|iq@sfr{3MEamkR^%RBwY$2l+lbjMI9N(be9n51N1TbAU!i` z?%S49L7j7EzVn^yM9JdzM$@#PuQcr~ymxbNx@KTz@wxu%Z% zCqcY0EQnBIz|x#$Bom9LoFMnLWji_wSee0gS4To=(R|P!_h(2)DSiZ_&I(pEgO$13 z>8BLdUr>PInxeC(co+_xJ%MGIx_nK;JWy4#(w&pq|AQ3Bq;W}nK`M3Uq>!jTCi9s{ z?ej>_30FJN;6hbtiz1Q{js@e$MRyU@ycKemwqhaj!fm%BD&o>Npp@;ec!Q&)tquXl zhVX(iC={{{6YaoZ)eLoGKqrhz7Ks?SR>zc#hh91Eoojs~Bj1A|{Q~nywv^N9b)7?| zE}&Ljss`J6bBeWpgYDMs`!86_1fpZ7oinFx89z;NM5@qBP9bfAJc^PN%gD1x){Lg4 z8kFj2TdE~6#e#5(lM0*rd_n|H5*o}{8O0flBXVI*a6twvVf+>cs+!BD(d)k~L33tN zQA-@n)&%Cx+yPN-x3M6Bh&LohS~!c7JjN5YhPg)GegEU9A%w7IrJs;FU6VC(L-3d= z|49+of#;m2RM1T=Vj(#7OCiNAy7%Uz#mJjN$LMJ4%_jXZav_t8;pc(p%|3iFp#^{% zJf?67sq#0usRZRtXQvuG1EI=T3jAtcaGVth7OHEPJ)>vBW2w26mx1)Pz-gX9W6Rr3 z$pMUq0vB>s8Ei~fkh+Z3R*kO^tmJ_NgF~%W>j&JW??+SkzR+&qeF@*w=li?sveZAf OClSYc*VYaGa_<+70JUNO diff --git a/Resources/Tests/SharedImages/plane-1-0.glb b/Resources/Tests/SharedImages/plane-1-0.glb deleted file mode 100644 index 53f8c61939e17278517655b238cb899b2652d9de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c+lt#T5OufoKNdZk#J7glFC|U0kR=;$lXNMBP-I88Rcy=1a=J?h^aJ{d{g{p< zJNIo%X~2#~(wQ@tGfuMk&6QyoKVKP!2hZ&+m~0S^B2rb1SIA#%P*}$?;SZF2L%wCA z@JWy$jw>RR8L_-%1u4Y*sU*mMZ9A@sB32i0+%u6-yJ$A(kNZ<(q8vYhQ+Ej~TENO$ zZp~u~`!6WKaYNzlC?19bcSm4%EK@#K$J$eATkV~aI{$+d$fR>gd`2qwPD!Cqe=O!R znL4MLo)Rv%u!Aerr433*K{ytSBOl!+(DUnOo~LL$hSR9GVZlT|Z+q^( zQfE-RX;q8$-W+3{-(Y)c))t5wzpV!eM7$+A*oC_|D^q;ORxsBnxbJ`bG=vmZtPV3WqbssPehd*4 z6+fxcCh?NfoC>;bOe_SaVJ)P&LHEIIG#>>MP>hKt!St>_Mm|(>KKwigg6W4Z7Dxc7 zAz}(2A=Uo2Hri0(cv~g#0t{WT9Qf6_;JBzVER<>&dK_&%^H7`$=Vb<(mLE15b``;7 zD4bepU#(Y|Sh&)3P#-mTjbJs8Bp71(0pHN~qY1oU7+3JTg!l39`@74w+<)#&A`Z_k Jty}#0*)LCbv+)1` diff --git a/Resources/Tests/SharedImages/plane-1-1.glb b/Resources/Tests/SharedImages/plane-1-1.glb deleted file mode 100644 index 655d287f64c970ee9693547e4a96e00af035ded7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c+lt#T5OufoKZ2f3;!7G@zmzo1LY8d2P12>Xgd#hNtzuh7lG9y6pdZjr?8kH@ z*|~39N&|K@lFppDoNQ5WK~c3@5q?j}HxxKF zik>71S1UoJHe;5TtRRJ)Ka~UpuU*f#QOv3Wj(awedKb+G{c(SaY?R|iaOy8$MGIIt zi>-Z3;r;~$IIbzWJBs_^z~2$LZO2xR({}b$#@2hMq~8A^1v2Ga5ucH&y;D*s)E|ra zOs3vxrl*99E$m>SyR<sB?`?wez*_Zw`lmhZn{T?dE`gZ58?_Ei2f#|bg1SDZrGBzcq-Cr-O864f)B z6Ei60YDaA=V2&l>6lVroM*NmYoMkkgvMNan7)KPqoM1r)EMxo{$GV#jjnM0V)q>{E z+)yhVEtUl4&e;M{?YHru1dDh>((HmexQnwg#kXt;bB)5g{>M*4NMXsUC?hkvBuf-X zUJ)HW5ow)w$!Sg{UDqa-lGCV?%G{v4a5kEc!U+h*Mw4)Q+aIF<3OOHs9)#ia!xsk> z0L&0Dg>R7Vep4D_=xDlIt?vR1Q?MNP^||D@5E+(Qv``%vrqMMc5490EFEfy|`mU+4 zMGS+Xae8HZHC|z0;Y!^=b5!FEf>k_LV2Gvl1HPf}M-zCzu&&^F3Gd_I_ji|Vwg23i NLL8o5S~vLfvtK*tv&H}b diff --git a/Resources/Tests/SharedImages/plane-1-2.glb b/Resources/Tests/SharedImages/plane-1-2.glb deleted file mode 100644 index b5047f4619f4d6687c104577a3971d9153b3225b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c+lt#T5OufoKNdY3$Cos;ekp01g)G^4o1{x2gd#h#tzuh7lG9y6pdZjr?8kH@ z+qrLBN&|K@lFppDoN|i(`1#5(y71i2g2@KqC?Y~IE|9<2ps-j2YrY*Rh9W$&qKTkoBcy8nX|$dq$Md`7DFPD!Cqe=O!R znYyQ$o)Rv%u!Du}(gr1@ARJ4^k&kW@==pWbS$>^LSqi_?NvKS#(1KQWzT!1bvW~t4 z9BaY_WzZ<(S~l83z*-EAFhB`oQY11(zSFf8<6bL=+PS0~mH7?~<(HTzN>X0;ZO_|R z>I~{Mt!lB|H^*4_H`rby-+#e6HV_>K?VSYes{Cn=6H=#MaSCOV@5&Aep?R;h>MP>hKt!St>_Mm|(>KKwigg6W4ZHb?-d zAz}(2A>IDAHnpL{>29^a3oz=6<-o7cCC7!xu+*wuxRwj!=+rY0#Su6!Gtjj9u&J;` z1e2k0dR6;sy~4!8m8OIBQG>4$tm2UZLo7ew8~T1Uf%gmJ3Z9qnKK^}wciC3^&z&j6 M;n}5ii$6d61yf_Q^#A|> diff --git a/Resources/Tests/SharedImages/plane-1-3.glb b/Resources/Tests/SharedImages/plane-1-3.glb deleted file mode 100644 index 1c796fb9019f726c22d4563ca4cac0c1276d57fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZ4Qp zeWlKzPTi^o+j(=0b$^5H*7E%qtYZVwVbI=5(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aiqFAZ-id|%N8_u zW|~^!Xuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_CT}%ry$`dY?WIAcZBX!jw$uk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&61=Hbd7>q$MCK?BmQSTP{P{`TfOFsxEAHUk5 z0APlIDSU%e_nXpaLq*fsDt+f*=z?Xyug(R>d6{CNL_7B^&z6r1GwJBThYFQAD^d`) z{JP2D%Lv9pq18%zY<$Dm!j-y>`l!Zh1gm%?!4Ofa)%pS7(htKi{Jt=*;C%_d$Itio Tmu0JOM_w$y!B`_5gdtJ|i(`1#5(Ja}$r!DNGQ6cHg97sy|1P*}w=;SZF2L%waI z@JW&&js=n0j96Z>f)sN8R1)OBcHFLsB32b}+%u8XyJ$A(kNZ<(q8vYh)9w;hw1Abp z+?vM}&R-5r5r*`|7I%idGfw%$7>b^iw`kSXVi_>5HTosvSK{#eXs zGIdWgJtbUjVFwG{r433*K{%F-BOl!+(DUn=WF378 zIM##<%AirmwQRJ7fVCJJVSp0Gq)23ne8;mD<6bL=+PS0~mH7?~<(HTzN>W|#ZLhnp z)EU%iTGe8`H^*4_H`rby-+#e6HV_>K-8~7~Rr%8#C!|ij;uOjz$)l_|ajddPRL^Km z>OrZmcGR{4=2#L=aaLo?klzuBvy4VlRwZcx7eU>EM|q!%8Z1gYJXbXg&%ipcoTPg6Um+Y>)s@ zL&Ow5Lc0BJZE8b@<88IT3oz=6<-o7cCC7!xu+*wuc$Np_=+rY0#Su6!Gtjj9u&J;` z1e2k0dR6;sy~4!8m8OIBQG>4$tm2UZLo7ew8~T1Uf%gmJ3Z9qnKK^}wciC3^&z&j6 M;n}5ii$6d61y@hA`2YX_ diff --git a/Resources/Tests/SharedImages/plane-1-5.glb b/Resources/Tests/SharedImages/plane-1-5.glb deleted file mode 100644 index fed618ea974ede9b0bd7c06fca5a1bf5ead9ba57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>x$Yy6i&DFJ!F1|n48k(PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-gv1~_2AuBUD?&?UWT{P?WM!hN0QHr0ysk4L?&0u9N zcls%X^%oT2xTbLT6c2;GvnQ|&Qa7@t~Ds?OfB1%zO`q@(au(DJjQ&*L4n+ zx`0}Bs~W8P_7rRX2HUOW`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE;>dBrqQZK9%@tIG*3X%^1CL( zEwJ_x=5KTkgMf OCK1PH*VYaG^6VEw$g|D> diff --git a/Resources/Tests/SharedImages/plane-1-6.glb b/Resources/Tests/SharedImages/plane-1-6.glb deleted file mode 100644 index 783d80d0007864368467dff5e4434c185df6c79b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dA^o*V<9~XwwswW>xRN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%a}wQ&Fd diff --git a/Resources/Tests/SharedImages/plane-1-7.glb b/Resources/Tests/SharedImages/plane-1-7.glb deleted file mode 100644 index d143bd58e73a56fd7dc7299f2eb7f08e5aee1422..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TXq!6iv7EJ!F1|I5F03L!nsff{U81+NB^;CUH!rB$+UoxVwnZ2k2w=L3(GB z_`Pi@4MgY8%(>@&oSQJ7-CP-l@$;2oyo2|4>Ww#u1Od%+E^_3~H^?tmD=O|8eLyZY zk^dyAw_4>?YBS(T##5Tg*;7W5``T_fCJK0w!g0q$QtzT^uRH8ckckrV2u_^^tXK*w zyx5w@6!u?Gfa8jyy`#AA_naMpZDCWr*us0N($;&Yq^0ifm3;pNYhxff4B9yfx~1}G2?=SLdLbB;P0~kM2#T#N4OP!r zLd!uZuQt`T0wzRK!AM+UGhd9TByr4w2`|DZg>ghK%n8Y9kH=ixkU)2HUJJeYS1qXT z%t~s7W9gE@+~F+{Reoy^O0Y=OB=s)1gS#ZoA~NDjm}}(Sbw7RXLkde?_%WTbC0!y{ zih}C!X&zO1XM!b6vUOz=DFyQjsmu+!^QME@z#D^LOf>c;qwWy7P{>*TOV9HrAHQNy z05C(q48B3S`*mqbLr2rzYJI0*lm$zGU!O}s(mW+7fXtZ9H9&6t)ws56tV|i5JO9U@OpuiAOqtW;Q-_j3*G5o$TuHbzMzsJw_ U_m^$8|J<2E9G+cT*ZA|ZUva6mMF0Q* diff --git a/Resources/Tests/SharedImages/plane-1-8.glb b/Resources/Tests/SharedImages/plane-1-8.glb deleted file mode 100644 index c94389505158c66c3f81a64625c5672c076d41d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>uTFD6!tpy9!0;6w!Qt6OIMr$V0a=^!z&FEW1vGC`zx>iK$4cz<^eEzTq{F(~i0X z9BaZ$%AirmHcYgIfRz{;VSp0GB#%XcJj*pD<6bL=+PS0~nfVS3ZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&J);NXXw@?h#VK)Cq@Zc}VUuB( zAxwtCsa5T(^$HUUSDFsiM-9G4u!@Hg46*!xZ|H~71m3T-D|lYR`}p_${bgJ3zjP)M Mhi8}8E&lxM7hfl|0RR91 diff --git a/Resources/Tests/SharedImages/plane-1-9.glb b/Resources/Tests/SharedImages/plane-1-9.glb deleted file mode 100644 index 3f2257e8a1d8191d1c1da07f6faeee83c945defe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1504 zcmb_c>uTFD6!tpy9!0;6<4YDA{~1}9LP{EUNmd3S4B1g^4cju3oVA3&9$*i-N7<2V z=iY6M8Jy@y`p$PQ-*KGIZm$f(`1!^#Zs5I}2ICFFQAC7bTp)kGL1DRC5&lTYH{{zU z3ZEqjRx3fIHY1i5EGM~~Jr@M|Zynb&QN+p|jyoojdKXRmy-{z1OqAg#aOy2!MRQo$ zi>-M~;rs;!IIbz$JBr6a-`f#5mTjunw(LDsW$V3DQulw50-18Ih|fsX-YF>*>W{^I zAyfA>(^JC57Iv`EUD}|StQWnB*wPGrhGPIzTt*>~E_QEeue?h~%+Mkngl@#kkkXp>{6mMrFPOL-{4bl2a&~Bu}#B#I}k&Rz0H` zsRpIG+EUvJm|;mc#c72tLViyqPE#69SQ#fdj3e@4POu<-mNI^eBi+q;BlP-TwV=5( ztEd%@=1T%|XK#V1_S<++fu4@xZ$!S}?~RZTg`5pO_k&>a;foCl z0A>i7!Z%2FzbQ>+=xEwot?wL+s$d!L>vPF*E>bMDXy;w4Ys2HfNIKQrLxBX&iWGFM zK5i;@5y5n5uwGRT8`m(kaHZ~IbyVXk1S@%@z!1<6_>R6Gj^X!}aRu*d_&t8Uzq@R! R{g=)Z;_&R!y1}2H{Q^|?w66dF diff --git a/Resources/Tests/SharedImages/plane-2-0.glb b/Resources/Tests/SharedImages/plane-2-0.glb deleted file mode 100644 index 582ddad0f62c63992412408c7b1cdbbff783612c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c+lt#T5OufoKZ2f(<4YP^zmzo1LY5@nCh1ZLp~#M6tJs#2i!Q>AXCm2@dc^cJ12!g{jr$O zWa^%0dQQ09!44L>OIs9^oNz1|M?SiXq35kNXIU$evJif|9aEW^wVaav)EkdKMPX-cChE8`@GaYR1M2^OT!QpRs_q`SFjgkJxv7BqL} z6}7_Ad_`dH>>Uu*ej5);u!uJ#%`UiuyErWpJZ3AHYZTn~K7JZN3M*EIDVfm~Ss`EY zlIZY>Nb0-`PBSX$rZ%yZoQ9=T<`&%tv*COgOh7Oungr8vZ-jg(W1eu#!g#46(F+z&G^$a02fa#tl3#;eGo1{_eW1_Mbab Nh~u+s>jr;$_6s}Uv&H}b diff --git a/Resources/Tests/SharedImages/plane-2-1.glb b/Resources/Tests/SharedImages/plane-2-1.glb deleted file mode 100644 index 21e6499216d916c745904fbf11f379f5bc5666a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c+lt#T5OufoKNdY3$Cos;ekp01g)B+DP12>Xgd#h#tzuh7lG9y6pdZjr?8kH@ z+qrLBN&|K@lFppDoNb^iw`kSXVi_>5HTosvSK{#eXs zGIdWgJtbUjVFwG{r433*K{%F-BOl!+(DUn=WF378 zIM##<%AirmwQRJ7fVCJJVSp0Gq)23ne5Y$G#=TY!wR1@~D)Su}$}cfbl%%}w+n%?t z)EU%iTGe8^Z;r9OrZmcGR{4=2#L=aaLo?kWYxjSw^E7tCF;UaYR1M2^M6)GRALkq`Udh3cdMPEokp7 zYHEd}#fre(*;^oL{I(tx5b>7eU>EMhMD!EDkWBOfZc7=9iE!R*5q8zcbK z5HW?1kZyllo7&LfbhldI1sHY3a^TnJlH)>TSZdWmc^uQV+ diff --git a/Resources/Tests/SharedImages/plane-2-2.glb b/Resources/Tests/SharedImages/plane-2-2.glb deleted file mode 100644 index 661b2c5dc047ffa113379cf11fb25a1d2d357646..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_cTaME(6b*yeN9FG*&7)M1A5=<5>Zomowv3Rf3OT8p7$tFJJ7q>yB^JOESq#@s z()R#DRC4NT``mLM$Bxta?Tuj=KVKO}2gcnjm~0V_A|eFi0{M$A3d{AH@CQo1A>TGp z_#{cNUJD|%8L_NjImzYxsUXOI?YN$aB39KV;Q zby2FLZMCd`8J2`ooL1N(>$~ej4IwBwL2^OT!QpRs_q`SFjgkJxv7Bpw( z6}7_Ad_~~i**hSr{WczyU=eRfnpJQIXK`92c+6ICuTgN{`}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLXc!PBJo6=NuTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&OOVs<>Q*JD?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcHvo`=~`|z@=zOrvmymatM8f$ zTSPDz8mCv4uf{73EL^D@SRK{)3c*SqDKNy+`T^h2_rnRiUl=#=yoC4Z@B6#!w%UL0 OOd*cXuB{vV<=HPoz_ZQ( diff --git a/Resources/Tests/SharedImages/plane-2-5.glb b/Resources/Tests/SharedImages/plane-2-5.glb deleted file mode 100644 index 3651e61c49c03f274df4739630870374af22d247..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&U8AephF#A*6sN>lk%Fe>hfRiE zhA<(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQSc?{tsB?`&YNSb^BZipmhZn{Z45+*LAxhGJ1T#cl89ERmx4jrBz=^npxDZ@NcD`R zv>KG^YFlk9U`iwvj3gB{55$N{k|Zpg@G^=s7)Rv6oREU{dBVjt33WH;jnM0V)q>{E ztfE#pmMtmF9o_;_?YHru1dBvN((HmexJ!~eCL_Ltxkmn7@6+c2q_E^=kkBbx(k1ew zD5(yg7IB?-E?CMWTh}I$Qm~+u%G{tke>$8E{V@o}L}Pz4>fIs_3OO5m>HGfV<5vs{ z0A>i7!8b^EzbQ>+=x91yt?vwss$ePb>vJhcRwP7f(ayV87ptdZI+jxnK2)e6X`X7jlxdMgRZ+ diff --git a/Resources/Tests/SharedImages/plane-2-7.glb b/Resources/Tests/SharedImages/plane-2-7.glb deleted file mode 100644 index b4a800a3be438c4d548546a3a6f9d345029751c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7uUwMDIZ6-4ZE6rC{cmaJOO3P z&zlUs2w*-GTCFONjc=G+xKj7AI;!y%f|WdwV2G&I`T^h455o!kzR<4VeF?wE&-eG2 SZMpy4nM53(U0OHz^Rr)y8MSl( diff --git a/Resources/Tests/SharedImages/plane-2-8.glb b/Resources/Tests/SharedImages/plane-2-8.glb deleted file mode 100644 index aa370f486fdf1f14b3a66a3d04fb93f0915a7c6d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXF1kinw+*`*d8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lgptIHh diff --git a/Resources/Tests/SharedImages/plane-2-9.glb b/Resources/Tests/SharedImages/plane-2-9.glb deleted file mode 100644 index e7b47423138bb6cddbaa3d402cd3bf03e0bd438b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mQ=(T15 diff --git a/Resources/Tests/SharedImages/plane-3-0.glb b/Resources/Tests/SharedImages/plane-3-0.glb deleted file mode 100644 index b7052e366bde9dfb5141a8e19608a2d0d6392613..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&gD3ImS@Yy)sqh;Dsfh%pltbh zlfjn}%!fj&mG;>9hPj0+bszOnjn@cP@koLpqE_n%d`mwJ$ME~YxPtd3{2o8w-(R-n R{&Qy%ad>uV-Qds9egS;lwPyeT diff --git a/Resources/Tests/SharedImages/plane-3-1.glb b/Resources/Tests/SharedImages/plane-3-1.glb deleted file mode 100644 index 9d072930811898df4fe326b11696917848dd5b9c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>uTFD6!tpy9znm2<4YDcGZMNmd3W4B1g^4cjuZoVJ9(9$=5T2icKi z=iY6M8Jy@y`p$PQ-*KGIudfWl`1#5(-obk_3nm+cqllCx<0bO%Hz=%DE5h$6c|g8x zqVP$OV6`fVP-eukg5@L^^QVF!|Fz?KCW=^T2-N?*$U?{)9JeHF3I&Zt) zzEWpUr*2h)?Yue0y1&78Yx({Q*0F);Flg^2XjkS>GaM70dc`S}O^`=XabjCV9?PE5 zjOanpSKD%10y8WKr#RKvBIIKtaGKI+%BncYVH}YUbAn6KXDQ>?I8xobZ-id|%N8_u z=9*gKXuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_Ca0%ry#bd!IfJAcZBX!j#PDk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&62D9OO7)(GgCYl7(ac_itDCB(br5^;-k6&$2 z05C(q6uv>K`%P)Ip`z(*mA-Q@bip#re}G!d|W;FP@xiMMGC@} zUpEuTFD6!tpy9znm2<4YDcGZ*>W{^I zCR6t`(^JC57Itu{y0k$F$qC1Tapa?$1bW_zIm=orA&Sy(w-YK>RcJvg+u!gSCuv(< z0**D|C1ubk=FqB_lo=8b~owr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8BT~!z2X$gCdi|xIB~2ZPh`(% zM)aWQt8KY0ff*KrQ=DpS5%LicI8A9ZVO6rqVH}YUbAn6KXDQ>?I8xo5H$t!fWeb`+ zGfgdVG+z>!J9`U6wco~r1R~y$9PGkfoE9rQVoR876x{VbeI7sxOIC#`nbIX$B0q+R ziHe_;t2*(5(~Jtbu1zcir(q?exj}cqbT}IZV^EBV#=&INyG1@!ayIzV4}!_ZuQo^k zs3BqsA0gHLrZ(D8;dHi2;5it&Vj1wObHQ<5rdTM|&RxqjJ9uPB diff --git a/Resources/Tests/SharedImages/plane-3-3.glb b/Resources/Tests/SharedImages/plane-3-3.glb deleted file mode 100644 index 888ce036664489bc196958e792820e62efca084c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c>u%F96b?hYN9Es9noFr5f1s33>a;eaEfZ2zAt!Ycqa=3x6@k&^h>{?2zUpB<;O>np=Be!en{_we3KgYgF8C?aLac!~V^28GpXMfg1>56HJo z6g~+OtX3rv%8Xc6u$<&#_EZq$zjj>DL=mfUSnirgs97}a_ilR=WTFf|f>Un+Bbvj= zUTn={3g<5e2?qBqtmT#*vS1V(58m#aY%$geXeC-Hxe9s?dT~w!h&uj?=c< z1RQI^OUj^8$hB;=g@Bb9YGHs9Zb=@C1o=+KmW+F?9BSv1Ze->=FqB_l9!p7iowr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8IFlgz2X$gCdi|xII*oFk7dtj zM)X0^TibG40y8WKr#RKvBIF|?aGKI+!m2pQ;W#26&Iv9_pQVgn<4ARL-Uz+^%N8_y zW|~^!Xuc$H?(8iP)qWcfQm}|OB+V?igS|K{5wWq>fE1Rj3R5zrOR_}1 z;1yBflQOCEE;!AopzGSiLU0;ZLYf(T?l+~;hKi=MRr=1s&;`qYU#$y{^D@OkiFWQ;o-H5O+>hjyAVV&3R-_x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dAxQ44chGWXdt|uQ#RN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%UfwQm3b diff --git a/Resources/Tests/SharedImages/plane-3-5.glb b/Resources/Tests/SharedImages/plane-3-5.glb deleted file mode 100644 index d9ac63cbf573e4342135140ac6082ab832ebf22d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TXq!6iv7EJ!F1|I7w96P$<^A;G$-$b}5LINgU&pBoihRyNd{YfIem)q<1EX z-`kebKy>cRoO|xaxryT0?Tuj=KVKQfTX^rL{&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&C+P34GDC9*7#~l+%y^E&3?yx&SCQ8U7ICU4WVkxZf zVrQOG*ndF*jw_1Rp5me3bN2+cg-!Kh3m>RTTkoBdI{$+d$dq$Md_k)A&PkzAe=O!R znL6j0o)a#1utRd)r7enRN(GTzAP?O|(DTMh@T9SpGRwVYGh%XG1QxWi`IT%)6gTxH z;Mh=+GY*YHj)l<<0@h-vgaJwz(lnB5B(7!O)G*3LEEsLc0ZD8D2kQj&68Z#wRw zQWsFWYE_MGy*|Y{zrl7Y`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04wDY#rHeJib>T$}!hYIB+$zl+; z`nsv$^AN^EqxGuvSo?;tg)3DX%cBZkB6uM}1%`+kjm8i7mcAd1;rE4c1Mf@tJ$=5v UyKbxf=gt)3`0U!c#$TTO0&sk_L;wH) diff --git a/Resources/Tests/SharedImages/plane-3-6.glb b/Resources/Tests/SharedImages/plane-3-6.glb deleted file mode 100644 index 2779bf34712a29ae2800aaf1bdadb4026c2bc172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c+m6#P5Dkm?kCmUJG&d^93zX88x@w!HEi0s|LQd)=MoAnwPIp&TB|d-;5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^M3q372TXW4ZkL{WO3PE18o1qQUT^9`?YoOaYD z;8+u0QU;Aewqc?z1gyl+2m_QbB6%zl*WzT3v z>OrZmcI37MW>^qTaav=GfRBm5X-dN>tKuYwaYP=>2`)*WrHtR;P<35b>7eU>EMpP_SQVsXMptBoya*yD zDt=NXP2vTo85MNhm{L|g5$hQu~4dAbd9blA6It_N3HC7{-IDM&WaTD zEkANHg))RGQ9jhF_S-VR^um>0lZeB!OY0VYe)bEOUbbHV diff --git a/Resources/Tests/SharedImages/plane-3-7.glb b/Resources/Tests/SharedImages/plane-3-7.glb deleted file mode 100644 index b3912f5656640be1c409c0e96f5cfd856ffe203b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c+m6~W5KXu0e;_|cAs5QFd1)zSC00PTKv$Izs+?egt0azWhusxI>Id{g`xzZO z3HNQQs*wU7+h@*P&LmEzH&=#X{Cs5?@8P|j1fvbYQAC7bTp)k8L1D365`Is~1M+PX zg-?+J{}%Qn?(TlSu+wDsO8srx@jflN79#Al>x@01h@^~Yj9 zlc{@}=_%oS3p-foE^SauGQzQB9Qo)rhMqT;oTZJGl)3Pm&6vtn5n9m7<~O{?anjV6 zfMZR#pbQ#?T+2pV2w01u5(X$?K(bh_kngl?#kkkXp>{6mMrFPOL-{4&-FN{SCHV$@gEdjtxYIL3<}byDER0;+T}F7o0-bBzcqtC$^PmvFaI3 zNjWIx)u!53z!XcuDNagk9`YfPI7w(UW<|WpU>uPTbAkowvV`#)9O-V(YN1#Ess;6( zX-TbcG+PjuJ9`U6mEYQf5-j31Nxcj1;4V(`6&|t$%ry$`I-frGAcX}h!h}rdf-I0P zc|mmeM69a3b52t#>AEtpl$?fzROSZV1(W`?AB;dSCK?6fVP}ARDCD&Fr5gm}k6&$2 z05C(q6uv>a`*mqbLr2rvYJF#5lm$zHU!O~kGm&7aMLWB;u5I;rre}G2<(A_QbrLwu z6Ogz1#;FuU1Vf^I=vC>rW`N;^D^)YgqY7UlSivI&hPWC(;CuR^KZ4&E#udCT;rICY V{_e7^_Mbabh{Ll>>l%N4_6vcawle?# diff --git a/Resources/Tests/SharedImages/plane-3-8.glb b/Resources/Tests/SharedImages/plane-3-8.glb deleted file mode 100644 index f4ac1a0480c3f7654b06afdcccae72dc8b1c56ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Qw~3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq678Qj-(b77eE$V&nLu(-2Zvv(it%?qUK>}v9%LTezF}_RO5MlmsK!?aR`NiCA);362YgH4kEZbZLc4+YCH$T~-``!g S<^FSL5^;QXZQbB6&wc@o4YhXw diff --git a/Resources/Tests/SharedImages/plane-3-9.glb b/Resources/Tests/SharedImages/plane-3-9.glb deleted file mode 100644 index 73333b2175f1e3bd3baa36d9bb94df456638efc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c?T*qw6s>D~4>G@-GHsDa|0p7xumQRPZcGR<(=wDXZKs(|aW{m-2k=3C4DU=^ zzE_R0Nn!5HoO|xaxsBr4?Tuj=KVKQfJ9zJ={&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&CUfYgqqL3FU9Jfs*^)8zBy2I`SnJ6KT;M85filwl^ zi=BB&VgCgMIIbvKdy0pC&)pN)7BiiEX$}IPq&4|f$5m?a5=2x;IQQXv* zfMY{N&Nws*ITl7c2w01u5(X$?NYhBJk!QECVmxT&SUcBrqcY!vq5P7FNJ+|Vb=vNs zQWsFWYE_MGy*b4?zrnUE`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04wDXSD!SL9oYq@HHopSo2OgTxi z7}TvkaVmp6gc;E;^s01Q6Ts}km8zHJQH3uNybz%RLs*T*5BQ$GAB^Gmg>eJ#OZYu~ WzQ4V0tNrKB6yo^o+PcPHp8W!djkYBK diff --git a/Resources/Tests/SharedImages/plane-4-0.glb b/Resources/Tests/SharedImages/plane-4-0.glb deleted file mode 100644 index 17b6f8b5e8bf2f2e30eea240041d383dfbda60c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c?T(W`6vZ{ZM>D?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcA=8 N5XWcN)(!sh>=!@4v&sMf diff --git a/Resources/Tests/SharedImages/plane-4-1.glb b/Resources/Tests/SharedImages/plane-4-1.glb deleted file mode 100644 index 69b3a0cc968c9af542ae3cac6890766754727527..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c?QWYe6lI0JOM_w$y!B`_5gdtJsU*mM?YLbNMXV~|xMw1%chP*%ANOa-L^*y0r`;v2XaOsG zxiya|oWGy|#|?$IqqrXqx;p~LvQ72amc6H{ZM}C&>i!Q>AXCm2@foSwJ0*oe{jr$O zWa^$~dP=z5!VVU?OB|q!%8Z1gYJU)XfX<=pcoTPgW04%Mm|(>G5kCTg4u^JHb?-d zAz}(2A>IDAHnpL{@wQsv1sHY3a^TnJlH)>TSZdWmc^uQTyn5!LI0ENo2AWnMHWjvr zU@|mLuWDbdSD0A1(sZysYVb9JRXkE)h~)=-L*I|4@P1)j!SfQ{$G`9IF57DVxif`0 MJiD}R@#km109QA&`2YX_ diff --git a/Resources/Tests/SharedImages/plane-4-2.glb b/Resources/Tests/SharedImages/plane-4-2.glb deleted file mode 100644 index 3666813a657a2c1670e48e5c7d05ea3d6c7255ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c?T(W`6vZ{ZM>D?(1&WRG$JE+Qx@pVW){RNin1N1V3@~J7sJm&J_y9hl596Hy z`n_t5O`vmU=G=2X&LvLgw>O4i{Cs5?9z1umV6sIxiii-53*;}hC@j}&!XGI4hJ4#Z z;gcl6dM$|5X2i0BB7>9Qo)jhMu?9oMo*<%0l?VD@k>{x82U6 zQWsFCZdHTz-kf6H-(b77eE$XO*g$j~bmuH+SLIJL9Fr>bl2a&~B#*M>#I}k&Rz0H` zsRpIG+E&{Nm|;mc#c72tLOv!Erzwr5tc;T!#u522Cs>d^OBuh#k?!WA5qkZvTF~5? zSJVne^A&-)vv)vL`)xcZ!6M#}G`rvq?&7pa@R+S&u2FE``}k=9DXdr-resD}WQBam zOQORkBB}E(IL)Y}o7%)uavGLWnOk%p%!cz}Fag1sXcA1vy%F-Ekn_Rkeh^GQe6c|R zzzhLX_y+0jH>Ifz9gVls`p&_q3YG!CK9?NlBE?dRcHvsC=~-Si@=zOrvmymatM8f$ zTSPDz8mCv4uf{73EL^D@SRK{)3c*SqDKNy+`T^h2_rnRiUl=#=yoC4Z@B6#!w%UL0 OOd*cXuB{vV<=HPou(Qqp diff --git a/Resources/Tests/SharedImages/plane-4-3.glb b/Resources/Tests/SharedImages/plane-4-3.glb deleted file mode 100644 index 81484dcb5c90b81a4b52576268e3f5de7ecb0582..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYW!tQ6U4&9(l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(<`byKZpRY9S9lSR)f3iV13`tosULtR?K|vKogx^#0fIL%2 z!IL0<6qQ6MGh|u8a*~VrQ$djT+Oi!Tg{;cqxThnbcF}CmzwJ+vjxziRPMsyJXbvlL zxz&#;tiPZD#|?$MqqrXqoE?E>n7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=@Qk~|g(@+{YsjC-vdYUh$}Wac|ClwV*TOG!EI+n%$p z)EU%jTGe9RH^*4}H`rby-+#ecCJ-G4?VJQ{%lv7EV^XJHaSCM<yPj0(DLOe_SaK_#TQL3jRaG#~jBP>hZy{&d{GMIKagKKwH9{prWACP)CN zAz}(2A=Uo2HnpL`akom~IT&@tGT>L|g5$hQu~4dAIEG`&$JSlLttTH!RN|~iLD};2 zCW9|Sm=A?ktJ-7h8|D_SG<~d(8hnjl6%QpCBI=%UTwQm3b diff --git a/Resources/Tests/SharedImages/plane-4-4.glb b/Resources/Tests/SharedImages/plane-4-4.glb deleted file mode 100644 index 0e156d7f198d315c5ee44e5b4c61433c86cacfa1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c?W)r-7_LXWkCES{Yr8tMe`L-b*kN0b+dK$kNZMs>q-{!)&T|+8FThLmVtkXf z`#vh-AYI-h&-=U|PZFo|+Z)3$e!emc560arm~0V_A|eFi0{M$A3d{AH@CQo1A>TGp z_#{cNUJD|%8L_NjImzYxsUXOI?YJEiMXbzWyK5q;SJABB8}+8hL>Ybrr=2CtXbv-b zxie2GoWGy|$2Enwr+66jJ9`4hvQ0H?%RW$5wq83Yb^iw`kSXVi_<~feos&YL{#eXs zGIh^0Jtti5U#~WzZ<(S~l82z*-EoFhB_-lE*SZzT?@7@t~Ds?OfB1%6t!o@=MHPC8>`0w%a*W z>H_N2t!l8|n^Ua&8*I0h@4sLj8;FjB?wkefs{Cn&V^XDFatdXW^wVaav)EkdKMPX-cChE8`@G>xg`~Cs>d^OBuh#k?!WA5qkZvTF{)C zSJVne^A&-6XYYWh_S<++fZfX-t$!SQw{YDs4xZ2iWDTR-ZgDU za4|Gas9EK!@mfLZ23A`&zCy5)M+ywFw0^)F`hGZp?+fDw#!L8~{=UDvZmad@_7viH L@7lbuTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&U8AeJhFi}(6sN>lk%Fe>hfRiE zhA<(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQScuPLb3zK*=Lr|rB-GuUH$t!fRSTLs zvx-{bShl1vcX$g#wco~r5-bu8NwW*?;4Vq>n2h)m<{J5Ty-%M9kiwFeK|-f&Nteiz zqNF-}TEunUxnL=iY+ai~O2L9sDszMG{ONEu^v56=6OH}JsCSDzDCBJLrSJQbk6$q; z0GJ_Q2Hzmv{iZaPp`&rPTHhHMRl!o=*XL4@tVoE|qMdiGE>@3gx|UlFK2)e6X`X7j}BJN&o-= diff --git a/Resources/Tests/SharedImages/plane-4-7.glb b/Resources/Tests/SharedImages/plane-4-7.glb deleted file mode 100644 index e1d4befddbb889c33bee1545db0938bb327038b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TVW~6t!FW9$|iq(P*=+6iS+AAxjdsNxBq5D5Dv5iaIim*db{d}cq@8G?id6NyoK|qRv@dCMv4f4w{B>bL|2jrSM z@}C6p!muDhnE^|4mXS=%pK^lS*H*{YQNYR!j=MS%Y8TD=y-{z9bd=&paB44MMKf5L z%dLJ)Vf_UKIIbz29mV~iZ|?{!!_?(#8s?s=ven)xY3F~C0-1C!iO)#o-YF>*>W{^I zCezMorl*9KG>4LcmH4wJ<;lBa%fTMy}3GygPPAntOBH1&V zl4?+@t8KY0fhiV*Q=C-T+~;E=aFWnq%E~CtU>uPPbAk)fX9?psI8fbOG(xZcWeb`+ z^NL#HXtp9Scjgv|YQK#K2}HahIoO4}ILTu?W-FL$x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXF1kincMZ21d8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lkfV0{F diff --git a/Resources/Tests/SharedImages/plane-4-9.glb b/Resources/Tests/SharedImages/plane-4-9.glb deleted file mode 100644 index b4369264d79c146b7bb3feb055fa49e08a601d8d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TVW~6t!FW9$|iqQDZ|Rg_5RO$dbfuk}ic1%4kNNqK=GXx=RT30s5GIklqOJq9M=@ip5kFR=qjven)>sr^4lflNA=#22J;@0=70^~Yj9 zlc{~4={ezQ2Rpb>UD~3EWQ1eEIC9Zl1U+wsoTaT;h`eyy?TCuF^bKfb`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`OtMJC$h90(G9I*YtetDRk(uwoP=0}VBqh~xdfm>U zQWsFGZdHSI-kf6X-(b77eE$V&nLuCe#u2$NC%7O3mN0&c1J%uCBlP-TwxGGQ zsHi25W@`d-XYPQg_S<-nK*Sr8qg}X*lRU-~wuZSz-hKb$ry-=UW~HByIbD-Aazlui zsQ5_{*NNwxrc}^PZDJuf^-CemExPySqs7RZf?{+u^=6a)7`af%#qjgM^JX8um>>b5 zhKMPAgjD;R+Ej)L$Jr@?XJAwnOMzdV3y!lQ!9uBa*)w`3JeKYlPBr*Yp#rCQ0>YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mU(6wp+ diff --git a/Resources/Tests/SharedImages/plane-5-0.glb b/Resources/Tests/SharedImages/plane-5-0.glb deleted file mode 100644 index c95fcb4cffbebdc8db0dcc30fbcd8d2523e404d6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&avZxxx1M<@PKmQ31x?Ejn+&@Q zVKNj>t!iJbSD0A1(sZysYVb9JRXmhnh~)=-LqCir@P4IT!Sfp4$G`9IFWYker89{* MJiD}R@#km109P-w`2YX_ diff --git a/Resources/Tests/SharedImages/plane-5-1.glb b/Resources/Tests/SharedImages/plane-5-1.glb deleted file mode 100644 index 16c81779b71e36a80f91e67bd31e0bfe891fea33..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>uTFD6!tpy9znm2<4Y1Y{~1}9LP`>MNmd3W4B1g^4cju3oVJ9(9$=5ShuM*A zH`i`sEMP}R(s#ad`HtgsaeZYN#?Lo~(She?9!$3gM-dT%ae@5%Eegx^n(#YH9*}RF zD14G6Sg!?<+KgCMu$<&_@l+7xcOBO=QN+p|j(aAOdKb+H{c(SWOqAhAaOy2#MRQo$ z%bj^l;rs;!IIbx=&lGpVf%i<{ShlGi+p_mmm96(qN!|ZJ3S`Q;B0eKkd#9vOs6Q6- zg-qSkOiu}yJJ`WOcWH}ak`s<4_QEeue?nB=icknePC#kkkXp>{6mMrHmChVo0yVQ*(_&f8c2)i~!!fB+FFA#>N%AO5PHd~lW7RX7 zk!nz?t8KNdfEkvAQ=C@VBIFYyahlR-#>zO!VH}YUbAkmKu$1v@9O-V}H$t!fRSTLs zi;7y|XucvaclHj5YQK#KC0N88l4cj&!Cjmd37)VO%ry#b`=34!A%zty!<5YFimZ?? zc}aBmL?m_I1*aL6bW@vHN>0O4Dszi&gZXGN3Z@_!6HSBJq(4SJ6ml{AG6;g%$FDXh z0GJ_Q3f~~z{iZaPp`+>Sw7zpNs)A*}ug@jNxk$0pqJ`=>X4mRgBM-F^I4e?+wEC{8 zutfxep>cXu`D(nvz`~Wffz?rsuMn){kpe?3tsn3W{VuTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&wqfgCqg&5B6sN>lk%Fe>hfRiE zhAcRoO|xaxryT0?Tuj=KVKQfTX^rL{&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&C+P34GDC9*7#~l+%y^E&3?yx&SCQ8U7ICU4WVkxZf zVrQOG*ndF*jw_1Rp5me3bN2+cg-!Kh3m>RTTkoBdI{$+d$dq$Md_k)A&PkzAe=O!R znL6j0o)a#1utRd)r7enRN(GTzAP?O|(DTMh@T9SpGRwVYGh%XG1QxWi`IT%)6gTxH z;Mh=+GY*YHj)l<<0@h-vgaJwz(lnB5B(7!O)G*3LEEsLc0ZD8D2kQj&68Z#wRw zQWsFWYE_MGy*|Y{zrl7Y`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04v~$;Tv3eY{ZMDn6hYIB+$zl+; z`nsv$^AN^EqxGuvSo?;tg)3DX%cBZkB6uM}1%`+kjm8i7mcAd1;rE4c1Mf@tJ$=5v UyKbxf=gt)3`0U!c#$TTO0&rZlL;wH) diff --git a/Resources/Tests/SharedImages/plane-5-4.glb b/Resources/Tests/SharedImages/plane-5-4.glb deleted file mode 100644 index f85acea0917ecd50f4d2b422fa66a50c4b4bee9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>uTFD6!tpy9!0;6<6FY!KO@UhNJ--^$;u#vAv?0IVOvI))0Pm}1MCs^Fgud% z+`EmjfE^u4-}%nvJC4)&&6TEUKi_DY3(xJ$pKK5gLsFKEm&jXeP*6n?;SZENBG1%O z@GOWQMI{l+3|UsNoaAEuToB}SE!)vi$f_KUdpZ(o7tIF!aes<*l;J0E>MUVJb6A|b}s2gX1)VM`32^&l$7JX>pA;M zok6XpRV~(idyKVzgY7l){THld0?}d6&PmX=%%5gBCUxo+r%*OQoZqRW}!{(3^kRg7(h5 zrj|IGuL#VYxdo!eZ|gw<5pPKjcHu5giv-`X70fmA@B5!V4L-f+^rIL4n|$E4EWW#;5aW+ER<>&uHoukqg&5B6sN>lk%Fe>hfRiE zhA(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ^LShl01kd+y1cXcGxDw_3squvzhD8*0U)LFueW-v3C zJN=Zx`U?thTvNDviibhp*%MfXsmoy+=7FlR)!I3!{Xa;7Ogfju7o>9SoD>T6$6~&a zsePX5IpJ~#E4WZy+MJT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2<_SnzzH7=3 z;bJIUA!n7Z#%l$s8(3}C_zJ;F9!fC8()s~!=!fA1zOS?!7_Z@b`uqO=x-HjV+LMUm Ly=(IZf4TPyONg`9 diff --git a/Resources/Tests/SharedImages/plane-5-6.glb b/Resources/Tests/SharedImages/plane-5-6.glb deleted file mode 100644 index c33df9153a2f8959d33c6428253e2f61bb4d82bd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Smq3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdPzu*RmZQ1+2{AxThnbcF}y$ANOZSM=5>;r_KsiG=r77 z+Uchh)?ZM7zgr+63+oIQbMn7VvT!#q$`w%R)(-2Zvv(it%?tB`R>5C!lQk zd6U5x0nCR&t5xN(@eOkeSL!}iM>W1eu#yK73=y?jKj2&Xel&&O7upTHFX8v}`Tp*@ SE%%>0lZfN9YwHGodG-sA<+XVL diff --git a/Resources/Tests/SharedImages/plane-5-7.glb b/Resources/Tests/SharedImages/plane-5-7.glb deleted file mode 100644 index b5542077807f81832e30683d6bfee9261830caf6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TXq!6iv7EJ!F1|I5FzBp-`-K!9~qh?NSgalQ@Fho0s5GIklvXj z>*uzmG!UITGv}WBac;tResg6Q#?M!V@fP0OnK#)Y5(G5QxyX^b*do7Juc^3a^c!-q ziTp=Nz4bb$Qkwx!GM>^@&L1<1+>YIHOcd}Uh2yS?q~1ldes9#9A`>O#0h~HZSg{mV zc)2r=DeS+X0LK+Y`-$Ry(085)Yzv#}#TMREmA2kHC2jo=QXo^#74aFV+B+qMLjAFr z&t%#<&GeLTxq}^&>n?3kNK-0^Lg<2LyI|x{dp%MltVMNnVM##0>STXLka;Tk4x>1=wfua18h)_w&X}{?@ z`%0Zb?W$EZw*C4T+xiW*Tgmrdur>yw!=Rm$pj#?`mXMH^sTYDl*(80Cg`n8V(opq` zCA1ur@@i9UD_}w-6^z6sHuJ@pN)pE`nDQcwQW!_%!kmzt_Ib?34GDBN7q!r|vg&)%yThSGA zr6{NlpXO1OcP3cEB->Obky0?fkjmVmJ8w3e54{Np#zYfuI_`~-3x%8yKKDIu`r!)( z1pqSy%-|cOyI+^4G;}oWoz`~>Mp>`~`1QFIB+X+YwP+XD)-_g-V|J`gIrvbaoFrKc z!d7256?`7Rcxbdx$Yy6i&DFJ(l?$VlFB+e~Ps(xTx8xT?!&)5+})&BoihRyNd{YfIea$re`LJ z_ian5kT_>%zVn^ScM>IwyBkf@e!kJPHaz!pZ@NV|2uM*dULbe5MSdBEgg;X94Y{U{ z{AWSDFf52rX28;%Wh4`e=bRw7W7&?50#;^l+|`j#yJ$Y>kNY#EqZB`ZQ)dM$n!(Ck z?etR$>n|w4aZSgd_gMr&PkzAe=Oz; zncC->o)fNiu!9TLr7em`MmQFXBNyFA(DU1nv-CC=A}`!lE21JUeFIwA`ieI=N?Pg? zaBK)KD1$~J+c41%0#;(Eg#k(!lPnT3a;>&084p@H*3LEE$jtX(D8Ilwl9F=TZ@bQ+ zQWsFGZdHSA_fE0)Z?N53zW;)?OdvWA+Bpl_mif~ZN2E%<cavDHU{6n^*`={ZdGCiyplBXfg7ppcoxZz1gHcMlMuxG5kF6yxE5@CP)CN zAz}(2A=Un-HkF~mY44Q4Gcc-(rNFPw1;<&DV4+mI>=`}1V|1#ShvF1C%@feH{IJQe zivT7=;nb?~)p&)8g)4OjtD_oUAy~-+35HmHz&G^$XbSIF+6_Ff;eGo1{_eUh_g^}b Nh~u+s>jr;$_6ug;vuTFD6!tpy9!0;6V>=fb{~1}9LP{EUNmd3S4B1g^4cjuZoVJ9(9$*i-N7<2N z=iY6M8Jy@y`p$PQ-*J@8Zm$f(`1#H-Zs5I}`r{44VMvOC@dA1C4GPNDittBDo{(po zD0mUXU#$uvlo_%#XBo-F>?J42Yddb!L?J6PIPRE8s9iMe_eQ-5GEs`3!D({=E1JQ| zUTn={3g<5)uj!JNJcmoj3W=-MbPu>6=&&nEJR*-jYdR8Tm}}jvhfYCag;RF zCE!>SUQh;&Lat?_Ed;E@PzeK+Fd|tbV&pk3TQcsoa;Tk4x{;ahz)*gHc_by(Y;`-$ zeWlKzPSvU!+qyZ%y1&78D*65k*0F);FzDt<(5}p%rZ^%x^^#L4n;_4kG2`i&GgKq+FxSX`=zaP;fE1Rj3=%S>OR_|s z;3ZMvlOnG2&N)q~pzF%SLU0@FMZ#ieEe#I z0)QC;rtl3?-LFfd4HZpmtMr|Lp$nD*zd9EjXGMaA679Teb!~VYvu(BY+(Us1oaPDW zT7KMQ>_rIEp}=aT9oDX4YT-)NMSWD^HG-8qlwb(x2Yg3A49D>M#<+s_E&Luo-``)h S<^F4D5^;ETXuTFD6!tpy9znm2<4YDcGZ4Qp zeWlKzPTi^o+j(=0b$^5H*7E%qtYZVwVbI=5(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aiqFAZ-id|%N8_u zW|~^!Xuc#cclH*DYQK#KDOkiCl4cj&!Cjmd2_CT}%ry$`dY?WIAcZBX!jw$uk}Q!g zctuqBq)h6(3r;gC=(;ws5S)gUkmd&61=Hbd7>q$MCK?BmQSTP{P{`TfOFsxEAHUk5 z0APlIDSU%e_nXpaLq*fsDt+f*=z?Xyug(R>d6{CNL_3$sb*-)~pN<}Us8ETsA_ZZ~ zubT|Mj9@$zTCKFl#y5;DT&dfrk7~R|u!=_#3=y?jtsn3${V*KE?+fD!-k0!u{Ct0Z T*_Qjyok_&u*`;-ZKR^2gZbr32 diff --git a/Resources/Tests/SharedImages/plane-6-1.glb b/Resources/Tests/SharedImages/plane-6-1.glb deleted file mode 100644 index 2c03127adf883478932512a62de7c3901aaffed3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&CQ}gkXpOYrES)>F2Yh|l1`G5Boii+?k=U^1NfLeh-W5A z@2esP($1Ng?|kR-oy6(l`byKZpRY9S9lSSlf4V_93`tosULx;)gMuoG2*0D`0ePm5 zf+s=zC@P6iX2`OFn7VvT!`xHVw%R)-wf_exkV)r~_>5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^L~q34~5v#gT{QIuY{8&i=~fdQ@Te#2`Vr(Jak zIM#%hltH7AZJ1~a0V^>y!T=?VNgj&?d6sKR#=TY!wR1@~GV>i6$}cdFrKBA9ZO_?P z>I`Z%t!lCEn`5l~8*HzU@4sLz6NnCjc20t}W&Sk7F{x9pIEAta@+c}!EThO{*)y7v zdQj@CUAZlR85V?7oYvSP;1eQnn$mE_syNAE9FYfef=e=BDdX2TRNcI9h2H$j7PNO3 zHMPXid_`dH%qyPj0(DLOe_SaK_#TQLAU;Vv>5qQP>hbI{%q18BM&OM7=9V}{_Nvd6C?oC z5HW?1kZONho7zy}xLYOg9E`eR8Stxf!Es)uSSZy(c`UtW^i27<_2ff|N}LrbC|iEs zWbkDO^P$jcReNlG!`#A^rjPYegRc>+;-LgXM4iqL_?CVcP2u;2b_MTC_&t8UzrSqD R{pZdk;_&R!y2YQL{Q`!~wQ&Fd diff --git a/Resources/Tests/SharedImages/plane-6-2.glb b/Resources/Tests/SharedImages/plane-6-2.glb deleted file mode 100644 index d41d5b589e5caf6801b9d2dab35c045befd86181..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?P}XF6!kjx9znm2BimWn%wS|$3Moz8C0QASFl0xuHEhesa@rCCdw@OW9%NUt zo$qdA%-}><(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQSc?{tsB?`&YNSb^BZipmhZn{Z45+*LAxhGJ1T#cl89ERmx4jrBz=^npxDZ@NcD`R zv>KG^YFlk9U`iwvj3gB{55$N{k|Zpg@G^=s7)Rv6oREU{dBVjt33WH;jnM0V)q>{E ztfE#pmMtmF9o_;_?YHru1dBvN((HmexJ!~eCL_Ltxkmn7@6+c2q_E^=kkBbx(k1ew zD5(yg7IB?-E?CMWTh}I$Qm~+u%G{tke>$8E{V@o}L}Pz4>fIs_3OO5m>HGfV<5vs{ z0A>i7!8b^EzbQ>+=x91yt?vwss$ePb>vJhcRwP7f(as&qF}qe5tEW>9K2)e6X`X7jjm$MgRZ+ diff --git a/Resources/Tests/SharedImages/plane-6-3.glb b/Resources/Tests/SharedImages/plane-6-3.glb deleted file mode 100644 index 91e683543dcf883c76eb7327245c6c0413489bf9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c+m6#P5Dkm?kCmUJG&d^93zX88x@w!HEi0s|LQd)=MoAnwPIp&TB|d-;5HUosvSK{#eXs zGPO@LJtbUjVF#D0OB)oEoNz1{M;^M3q372TXW4ZkL{WO3PE18o1qQUT^9`?YoOaYD z;8+u0QU;Aewqc?z1gyl+2m_QbB6%zl*WzT3v z>OrZmcI37MW>^qTaav=GfRBm5X-dN>tKuYwaYP=>2`)*WrHtR;P<35b>7eU>EMpP_SQVsXMptBoya*yD zDt=NXP2vTo85MNhm{L|g5$hQu~4dAIEG`&$JV<>SFPN7{-IDM&WaTD zEkANHg))RGQ9jhF_S-VR^um>0lZeB!OY0VYe)bEO4z^za diff --git a/Resources/Tests/SharedImages/plane-6-4.glb b/Resources/Tests/SharedImages/plane-6-4.glb deleted file mode 100644 index 7336fd6535a6ed885281ababd7c3292d5e9c42a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?P}XF6!kjx9znm2BRgK$%wS|$3Moz8C0QASFl0xuHEhesa@rCCdw@OW9%NUt zo$qdA%-}><(z)k;oa-o=U0)f7@$;2oyo2{<>W?>wgdr^oE(+w$Hz+7qD=O|8eLx;I zQScuPLb3zK*=Lr|rB-GuUH$t!fRSTLs zvx-{bShl1vcX$g#wco~r5-bu8NwW*?;4Vq>n2h)m<{J5Ty-%M9kiwFeK|-f&Nteiz zqNF-}TEunUxnL=iY+ai~O2L9sDszMG{ONEu^v56=6OH}JsCSDzDCBJLrSJQbk6$q; z0GJ_Q2Hzmv{iZaPp`&rPTHhHMRl!o=*XL4@tVoE|qMf^zYj&+JR*zc^K2)e6X`X7j|Z~N&o-= diff --git a/Resources/Tests/SharedImages/plane-6-5.glb b/Resources/Tests/SharedImages/plane-6-5.glb deleted file mode 100644 index 5283d5b7c43f57378feb3665a551e40b91ae0bf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Smq3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdPzu*RmZQ1+2{AxThnbcF}y$ANOZSM=5>;r_KsiG=r77 z+Uchh)?ZM7zgr+63+oIQbMn7VvT!#q$`w%R)(-2Zvv(it%?5C!lQk zd6U5x0nCR&t5xN(@eOkeSL!}iM>W1eu#yK73=y?jKj2&Xel&&O7upTHFX8v}`Tp*@ SE%%>0lZfN9YwHGodG-sA<+XVL diff --git a/Resources/Tests/SharedImages/plane-6-6.glb b/Resources/Tests/SharedImages/plane-6-6.glb deleted file mode 100644 index ac18670568591b3027f4cdbd632c0b9b00560afe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c>u%F96b?hYN9Es9noFr5f1s33>a;eaEfZ2zAt!Ycqa=3x6@k&^h>{?2zUpB<;O>np=Be!en{_we3KgYgF8C?aLac!~V^28GpXMfg1>56HJo z6g~+OtX3rv%8Xc6u$<&#_EZq$zjj>DL=mfUSnirgs97}a_ilR=WTFf|f>Un+Bbvj= zUTn={3g<5e2?qBqtmT#*vS1V(58m#aY%$geXeC-Hxe9s?dT~w!h&uj?=c< z1RQI^OUj^8$hB;=g@Bb9YGHs9Zb=@C1o=+KmW+F?9BSv1Ze->=FqB_l9!p7iowr?Y zU#T;wQ@5(YcHSIg-QQrlwS4~t>)1ea7_@g1v@7$c8IFlgz2X$gCdi|xII*oFk7dtj zM)X0^TibG40y8WKr#RKvBIF|?aGKI+!m2pQ;W#26&Iv9_pQVgn<4ARL-Uz+^%N8_y zW|~^!Xuc$H?(8iP)qWcfQm}|OB+V?igS|K{5wWq>fE1Rj3R5zrOR_}1 z;1yBflQOCEE;!AopzGSiLU0;ZLYf(T?l+~;hKi=MRr=1s&;`qYU#$y{^D@OkiFV$#y0&~e=6)ov1Q~LPvmym~ z%R5fl5gZcbqm(1Z#%lztcqG9PSL+Adryqu6_`Wc%;C%_-u%F96b?hY$K>Bpnj0PD50uhLo7!fyWkRYd*WzT3v z>OrZmcI37MW>^qTaav=GfRBj4X-dNhtKuYwaYP=>2`)*WrHo(WP<35b>5|Zx`<3v`FxXEn%*af7ko;c>pOaSrw#YN|$7bya*yD zDt=NXP2vTo85MNZm{gJN_v_9vs>5P49^+2Bjx_a`5}njit7 zhKMPAgjD<6+SG;$$K5D_=U~(o%Ya{<3y$+L#X_leeq~&l@^SR8(N!zAo_{D*iL)XF zeanxWOrZ>6N|X<^s{OVMFuic4>1KV@;A;e{cqqXT*AMufez+OK?+fh$-k0!u_ephmW6 diff --git a/Resources/Tests/SharedImages/plane-6-8.glb b/Resources/Tests/SharedImages/plane-6-8.glb deleted file mode 100644 index 2a1b3ae5e9acf0b6794054820f6ae5710761b8dd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Yy6i&DFJ!F1|m|Mk$Lb28b7d2b8OF^Ve;+RZHGGQ{YyNJ*S=wtRldS;S% z-?o$nqH|{EJKwo{Cs8uLz0ox7=PONn3-8^`pKK8hLsArs7sy*|QBbbegg;R74SA-H zf+s=z^|~NJnITJamXS=%pK^k{*Ou+*C}d>@#~mFBwTou`-l#W4I!f^)ICYk=q8Y5r zOJq9M=@?p5kH9clHF9Ve0ZV4f8-%*=p~c)czl&Kqj3_;tNu_cTNh0`eQMl z$<#j2^qg?HgB@I`E^SdnGQzQ79C_$2f}Xe5oTaT;h`jLH?TCuF3=C*x`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`M6yW4$g^BiG9I*YtetDRk(uwoP=0}VBqimzZ#vGQ zQWsFGZdHSIyQf(DH`q=s-+#ecCJ-G5?VJT|%lv7IBT}VaatdV=Y+j3h1Q!EIlIH|CCz{f=3B%$Gyl~J6*I3f?`1Q(>w62@JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob%N-}k2`)1xtZnoePe$BEdq5cF{GuddKLP@^P!dhYA%q%@Yu| z{JP2Dix9>`q1CGL*!YIAg)4O%tD_oUAy~;n35JMTt=13tmcAcO;P-`g1Mf@tJ$=5v UyKc+<=guVJ`0U!c!C#*J0)B?IQUCw| diff --git a/Resources/Tests/SharedImages/plane-6-9.glb b/Resources/Tests/SharedImages/plane-6-9.glb deleted file mode 100644 index 975cf35dcce6f3b63f8a644e39a22f963942f6b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c>yFYu6s~K04>G@-(hCylA4OynHb7UvjR_%UTBhxow$n_fxEn&^1NfjmhG(WN z_f=zTQkXL{-}%nvJB^aX?Tw~sKVNCuJ9zKr-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?Sk6)HNHZyk_QqDVYPn1_w@Z}3coM38+c#B@9FdX U?R8u3KX)b($7k2p4gT`%7pFx$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7jhi0jcZdrPBr;Zq5`LR0?L-3 zHyL~pzuTFD6!tpy9znm2<4cxqW-ziWg_b1llB^6$7_y_-8n$I*Ic*7nJ-{Aw53(c4 z&b`|hGdR(a^qucqzT-HZU0oQ4@$;2oyo2{<8jRNnM-eGY#!KYiuTfYnmxSL@@_>BX zMB$Sl!E#v=q0ES71Zes9>DAQNTy5uAE+SkW9- z_IzU=QaFD>0gh{m&X(eC(D$|kj%AzjwJm!`rERr$OzQp*QXrGgCGiQV+&dQiT?@vi%LOaGbW) zCE!>QUQz~)Lat?_4Fs&jPzwW;FeG^_668A_TQcsnvag+Ux{;Z0!BBpIc`PO6b>4Qp zU8PQ-PTi^o+j(<{b$^5H*7E%qtYZVwe$d`g(5}p%W;iA~^@>v{n;?&(;>5O!JeED9 z8PS8HueRm31ZG$ePI0QSMaV});54PtgjI2p!#E-z<^-3d&r-&(aHP6<-w3__mn~@S z%rv#c(R@K*?(7W^)qWcfQm}|OB+V|kgS$8_5Avg^yAtGCmG0`}fjCw=lLm_8_Fa02xeEe#I z0)QC;rtl3?-ET^x4HZpiqx7AFp$nD)zd9Ej=Vgk85-n86F)yu4TRxs1e5g=~vmymy z%deXZzKmcz6k4sc$Hq5|EnKPFsE=yAMzD%U5)2WwTCE@OE&XskhTj*)1-vie_wf1t U{=6;spF5L?{j+oH27h|?3vpGoMF0Q* diff --git a/Resources/Tests/SharedImages/plane-7-2.glb b/Resources/Tests/SharedImages/plane-7-2.glb deleted file mode 100644 index 4277e4d44c9356deb4077a77ec817b0de31f6a7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&82oV2vTboy0p#O)F!brK7fzugLr0= z^u8)$Anlx)`ObGP-$|6rZ>}^=`}s=K-obl2^ClaFgMbtT;{|dT8|0T^NccS^56Crj z*>W{^I zCR6(~(^JCb7Itu_BXu7QPNhI zfMZQ~K^Zg(*@lU>5U>(MEeue?h-8t7k!y8K$+*|bp>{6mMrOVPL-_^fk(89vdE0gN zl{$l3b*ma|=gl$J{tdQU%lBWfmI*|MK|3cw+cJNe;)qnKmz+Y`1bGxCCzg?Ck?a{w zNi`_d)wbN0z!VF@DNZVE?(;DbI7w(QWn~m+FpkKDIl%?#vxM;*9H?$C8ll(!vIWhZ zc||R8G+PmvJ97&}wco~r1R~y$9PGkfoa8YcvlYxW^6q+{J`W&;6)XLO%;<`&kQ+k8 zM8!{vxK2FhG^K*BYZD8>sb30dZqS`K8_tK`1QesAi8mehM#zOq&Ie!mo;UsY)dUFu zHAGC|Bc$5j)TT02IGwE$cm_sQu@v~#x!^b}5-gNz7q(&R*T%IeAE%mpC{cmaJOO3P z&zlUs2w*-GTCFONjc=G+xKj7AI;!y%f|WdwV2G&I`T^h455o!kzR<4VeF?wE&-eG2 SZMpy4nM53(U0OHz^Rr)x*|l^4 diff --git a/Resources/Tests/SharedImages/plane-7-3.glb b/Resources/Tests/SharedImages/plane-7-3.glb deleted file mode 100644 index a16497fd90219fcb5389bcf970dcf4cd62ff4844..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c+m6~W5KXu0e;_|cAs5QFd1)zSC00PTKv$Izs+?egt0azWhusxI>Id{g`xzZO z3HNQQs*wU7+h@*P&LmEzH&=#X{Cs5?@8P|j1fvbYQAC7bTp)k8L1D365`Is~1M+PX zg-?+J{}%Qn?(TlSu+wDsO8srx@jflN79#Al>x@01h@^~Yj9 zlc{@}=_%oS3p-foE^SauGQzQB9Qo)rhMqT;oTZJGl)3Pm&6vtn5n9m7<~O{?anjV6 zfMZR#pbQ#?T+2pV2w01u5(X$?K(bh_kngl?#kkkXp>{6mMrFPOL-{4&-FN{SCHV$@gEdjtxYIL3<}byDER0;+T}F7o0-bBzcqtC$^PmvFaI3 zNjWIx)u!53z!XcuDNagk9`YfPI7w(UW<|WpU>uPTbAkowvV`#)9O-V(YN1#Ess;6( zX-TbcG+PjuJ9`U6mEYQf5-j31Nxcj1;4V(`6&|t$%ry$`I-frGAcX}h!h}rdf-I0P zc|mmeM69a3b52t#>AEtpl$?fzROSZV1(W`?AB;dSCK?6fVP}ARDCD&Fr5gm}k6&$2 z05C(q6uv>a`*mqbLr2rvYJF#5lm$zHU!O~kGm&7aMLY8>&sL9XUR&3C<(1rW`N;^D^)YgqY7UlSivI&hPWC(;CuR^KZ4&E#udCT;rICY V{_e7^_Mbabh{Ll>>l%N4_6vbVwle?# diff --git a/Resources/Tests/SharedImages/plane-7-4.glb b/Resources/Tests/SharedImages/plane-7-4.glb deleted file mode 100644 index df1f59836c150937f33d2530494a1f532a1bfb44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TVW~6t!FW9$|iq(P*=+6iS+AAxjdsNxBq5D5Dv5iaIim*db{d}cq@8G?id6NyoK|qRv@dCMv4f4w{B>bL|2jrSM z@}C6p!muDhnE^|4mXS=%pK^lS*H*{YQNYR!j=MS%Y8TD=y-{z9bd=&paB44MMKf5L z%dLJ)Vf_UKIIbz29mV~iZ|?{!!_?(#8s?s=ven)xY3F~C0-1C!iO)#o-YF>*>W{^I zCezMorl*9KG>4LcmH4wJ<;lBa%fTMy}3GygPPAntOBH1&V zl4?+@t8KY0fhiV*Q=C-T+~;E=aFWnq%E~CtU>uPPbAk)fX9?psI8fbOG(xZcWeb`+ z^NL#HXtp9Scjgv|YQK#K2}HahIoO4}ILTu?W-FL$@Fho0s5GIklvXj z>*uzmG!UITGv}WBac;tResg6Q#?M!V@fP0OnK#)Y5(G5QxyX^b*do7Juc^3a^c!-q ziTp=Nz4bb$Qkwx!GM>^@&L1<1+>YIHOcd}Uh2yS?q~1ldes9#9A`>O#0h~HZSg{mV zc)2r=DeS+X0LK+Y`-$Ry(085)Yzv#}#TMREmA2kHC2jo=QXo^#74aFV+B+qMLjAFr z&t%#<&GeLTxq}^&>n?3kNK-0^Lg<2LyI|x{dp%MltVMNnVM##0>STXLka;Tk4x>1=wfua18h)_w&X}{?@ z`%0Zb?W$EZw*C4T+xiW*Tgmrdur>yw!=Rm$pj#?`mXMH^sTYDl*(80Cg`n8V(opq` zCA1ur@@i9UD_}w-6^z6sHuJ@pN)pE`nDQcwQW!_%!kmzt_Ib?34GDBN7q!r|vg&)%yThSGA zr6{NlpXO1OcP3cEB->Obky0?fkjmVmJ8w3e54{Np#zYfuI_`~-3x%8yKKDIu`r!)( z1pqSy%-|cOyI+^4G;}oWoz`~>Mp>`~`1QFIB+X+YwP+U|t7BeU*H}GHIrvbaoFrKc z!d7256?`7Rcxbdu%F96b?hY$K>Bpnj0PD50uhLo7!fyWkRYd*WzT3v z>OrZmcI37MW>^qTaav=GfRBj4X-dNhtKuYwaYP=>2`)*WrHo(WP<35b>5|Zx`<3v`FxXEn%*af7ko;c>pOaSrw#YN|$7bya*yD zDt=NXP2vTo85MNZm{gJN_v_9vs>5P49^+2Bjx_a`5}njit7 zhKMPAgjD<6+SG;$$K5D_=U~(o%Ya{<3y$+L#X_le-Zi?Wd|ds?xKb;po_{D*iL)XF zeanxWOrZ>6N|X<^s{OVMFuic4>1KV@;A;e{cqqXT*AMufez+OK?+fh$-k0!u_epggu{ diff --git a/Resources/Tests/SharedImages/plane-7-7.glb b/Resources/Tests/SharedImages/plane-7-7.glb deleted file mode 100644 index 00de2f419649b4ce14431bdeecfca344225e6826..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c?QYXB6b(bXN9Es9>NKT;{DD$BsZ-mGwoFJ>g`Ct)jFLEVoNiN9i3i|8c?_H&=#X{Cs5?@8P|jdgBcuK|u4IiyXQ04f2cCii&$iACQYp zkhjUWTJ#Tf>UP!BbLGl zFSh0}h5Z*4;JBh_?+wDsI6Y3qNG0-18Ih|fsX+$kv(>W{^I zCezkwrl*99EzFQycWHw{no>a|7sy4oA@sbl5OMCo8QQqgmF`E z0**BmIpfeM)Uq(zLcm%Kl`udFLz;#%LayD$igB-%L+xDBjmmrnhVn}yLM17u{kG%m zD|H67t5((6_M2mD>o?d=CEtI++8BrqgLY1WZmIlPLPA=mUI+$dlk`y*f?_L6L)9~u z(DI;^TbpWH0TUvrU?eWFnJ-3Ek~n6;gco6y!f`|{oD-7M9*?=WA%X7ZycT-(S1qXb z%t~s7W9gE@xx-r^s{Ga-lwgslN$Oc}2YX4JMP$U6aITSe*ZuUl4=F5p;m35!mUM|+ zDGI8?r+HN6oe7pO$<~!gq!i3Aq%t??&YKQq18)q1G11tYjJiYQLLq1UFFntjeEf<* z0l*9aGq{6v_v_M>hK{Da)%s4sC<~SVzh0Muq77R2`c2ABv}md zR(G7X12`nwN3KSt-RLv}xDtw9Hg$NWF;%fYW`}D(L4Br>V6}&Iud;ENVciC3+ Q&#fuM;o7BfjXz)e1&W8ZJOBUy diff --git a/Resources/Tests/SharedImages/plane-7-8.glb b/Resources/Tests/SharedImages/plane-7-8.glb deleted file mode 100644 index 5d110b0a72afb7f166191f2500c13843254be4e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 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 M@a)pM#h;)30$%U50RR91 diff --git a/Resources/Tests/SharedImages/plane-8-2.glb b/Resources/Tests/SharedImages/plane-8-2.glb deleted file mode 100644 index 996dcf6ea7039612203326b0d5c612b438d9a532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE^NcryGFMfd8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lgaI@9` diff --git a/Resources/Tests/SharedImages/plane-8-3.glb b/Resources/Tests/SharedImages/plane-8-3.glb deleted file mode 100644 index c01f821a031a193fb260cb7b123ad7b58de769ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Yy6i&DFJ!F1|n2Qw~3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq678Qj-(b77eE$V&nLu(-2Zvv(it%?TezF}_RO5MlmsK!?aR`NiCA);362YgH4kEZbZLc4+YCH$T~-``!g S<^FSL5^;QXZQbB6&wc@nv9)&q diff --git a/Resources/Tests/SharedImages/plane-8-4.glb b/Resources/Tests/SharedImages/plane-8-4.glb deleted file mode 100644 index 7c325c9d370634b6fb762edee6f31173813eac16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>x$Yy6i&DFJ!F1|m|Ml>PqEeo7d2b8OF^Ve;+RZHGGQ`tcM+iv&`0dU^vopj zzHKQL66eg!cfND^PNHOfd!uRE&o`Rp!gDwCCtHNWkQ4>u1@abK6qM^V;SZENBG1%O z@GOYGUKd0tGh}JbGLnh;b54-=*0LQPg{;irxT7PXcG0Zg8}+70M=5>+r_K^qG=r77 z-07zj)?ZM7gd_gMr&PkzAe=Oz; zncC->o)a#2u!9TLr7em`MmQFXBM;q0(DT-sv$Pcpkr!UO9Z?aNfdQ>-f5RIbC2e&H zI5vbAltH7AZJ1~W0V^@o!T=?VNEV40d6sKR#)DRlwR253GV?tc$}cdFq@*18UB@|8 z>H=!jt!l7t_Y`aY2HUCS`!86_1ft`howJ~AnLkZ&M5@$FPN8gqJc*JM)5x<(_Kc>a z8kFj4TW(8WiUr{mClxji_?QTsBs83|GKw=8N94hr;DYp7!uTx?RW}!n(CdHMg67V= zqLw(Ctq9DWxdWowZ{tA<7V(Cp*#&oS7bkg)$7}_2jr{xGr_TdOVa3WIAv3xnE941Y z5*0ov;yUk~)07IjsZA^dr$H&CxkdN>Y&akK6A+A!CjNBX8zB!0IUjuK`~LLfR}&Nf z%n&ezZ;fXE?mRayGFMfd8kc+(>wu5%kP>D zy9i-06i%%wUyWB7Sh!L*usW*o6@ryKlwgRZ^#i`4ABGcnztV2tc@6K=-}m>|ZMpx_ OnM53)U0XN!%d=lkV6)l) diff --git a/Resources/Tests/SharedImages/plane-8-5.glb b/Resources/Tests/SharedImages/plane-8-5.glb deleted file mode 100644 index 8916d5b2a57d2a1b5270e12acc065b34352fb112..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1500 zcmb_c>x$Yy6i&DFJ(l?$VlFB+e~Ps(xTx8xT?!&)5+})&BoihRyNd{YfIea$re`LJ z_ian5kT_>%zVn^ScM>IwyBkf@e!kJPHaz!pZ@NV|2uM*dULbe5MSdBEgg;X94Y{U{ z{AWSDFf52rX28;%Wh4`e=bRw7W7&?50#;^l+|`j#yJ$Y>kNY#EqZB`ZQ)dM$n!(Ck z?etR$>n|w4aZSgd_gMr&PkzAe=Oz; zncC->o)fNiu!9TLr7em`MmQFXBNyFA(DU1nv-CC=A}`!lE21JUeFIwA`ieI=N?Pg? zaBK)KD1$~J+c41%0#;(Eg#k(!lPnT3a;>&084p@H*3LEE$jtX(D8Ilwl9F=TZ@bQ+ zQWsFGZdHSA_fE0)Z?N53zW;)?OdvWA+Bpl_mif~ZN2E%<cavDHU{6n^*`={ZdGCiyplBXfg7ppcoxZz1gHcMlMuxG5kF6yxE5@CP)CN zAz}(2A=Un-HkF~mY44Q4Gcc-(rNFPw1;<&DV4+mI>=+%rXY{I>hvF1C%@feH{IJQe zivT7=;nb?~)p&)8g)4OjtD_oUAy~-+35HmHz&G^$XbSIF+6_Ff;eGo1{_eUh_g^}b Nh~u+s>jr;$_6ugmvx$Yy6i&DFJ!F1|m|Mk$Lb28b7d2b8OF^Ve;+RZHGGQ{YyNJ*S=wtRldS;S% z-?o$nqH|{EJKwo{Cs8uLz0ox7=PONn3-8^`pKK8hLsArs7sy*|QBbbegg;R74SA-H zf+s=z^|~NJnITJamXS=%pK^k{*Ou+*C}d>@#~mFBwTou`-l#W4I!f^)ICYk=q8Y5r zOJq9M=@?p5kH9clHF9Ve0ZV4f8-%*=p~c)czl&Kqj3_;tNu_cTNh0`eQMl z$<#j2^qg?HgB@I`E^SdnGQzQ79C_$2f}Xe5oTaT;h`jLH?TCuF3=C*x`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`M6yW4$g^BiG9I*YtetDRk(uwoP=0}VBqimzZ#vGQ zQWsFGZdHSIyQf(DH`q=s-+#ecCJ-G5?VJT|%lv7IBT}VaatdV=Y+j3h1Q!EIlIH|CCz{f=3B%$Gyl~J6*I3f?`1Q(>w62@JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob%N-}k2`)1xtZnoePe$BEdq5cF{39rhHtzYjmr@hYA%q%@Yu| z{JP2Dix9>`q1CGL*!YIAg)4O%tD_oUAy~;n35JMTt=13tmcAcO;P-`g1Mf@tJ$=5v UyKc+<=guVJ`0U!c!C#*J0)9%hQUCw| diff --git a/Resources/Tests/SharedImages/plane-8-7.glb b/Resources/Tests/SharedImages/plane-8-7.glb deleted file mode 100644 index 1b60bb7f7c9f36e5d4e3b2037bc507b808074840..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c>x$Dr6pkytN15Lt&82p05Tw>FbZMKlt&32KOwvg*l4QbUQgjTxi{S+90a5&7%z~!+##0_NJlAt0H@9hRy2c^ zx!UQ+6xLr*fa98?^F(n!95_z|mSO7hH4Sr5RoQCql+^woq(CN}OX4$9xpztmh5BPL zpUKod&GeLTwSyg8s4i_$L^8s$U>v#VHiDkFLeA1wEJR+o?RG>(T>1vIvi%iraFn#w zCE(Z)UQh;&LbhR|9R#ezPzwW;FeX_fV&qyKQ!?(ga;Tk4x{;YbfuZ~Y^GHg{>AdMW z`%0Zbt-4hWw$nSt+P}eeYx({Q)-r+UFlgr_Xj|q_Qyh^h^^#L4n;;LOJT!IQ2^*%`Lj~=A*^Pn}T9=H1%ea{usGX$;I&V!1HDwzL+2Z zpoWMke1uf{o7z-{3a7JE0?)vxDwYDjIu{&gMS_J=?ef~VHs#~!J)>7mK9s1yX`XyTXUj#583awU^$Hq6zEnKPlSRK{)3c*SqNH9dyYW;w3>HE(H z^PaYp2IS~UI``bik)mXNd!uRE&o`Rp!nm9HlP$twNQ#2-0(pxq3d;4G@CQmBk!R{C zcoxK8uL~lS8L~8I8Og-_IVZ?_YuS#DLRMz5-O-Uyt7z8mje1k0qZB`ZQ)dY?n!(Im z?(|a%>n|w4aZTawDINxWXHQ@mrY?tRm^_WvLSGU;3rUy#bRb5bbOAB*`y zruKQJ=Y-20tl&a*X^SF~5sn4p$U}D#^t`p^EN#U?a7@t~Ds?OfB1%zO`q@(au(DJjQ&*KrP& zx`0}Bs~W7^J;mC;!FFo-{tMPJf#^7B=PYPj=1)@`kt+3)Qz)AtPom_+H1aHxJ)JT!I1Nf6%`Lk3XT$l>pMYR=H1Vh7-UxY6$ob$)-}k2<_SnzzH7=3 z;bJIUA!n7Z#%l$s8(3}C_zJ;F9!fC8()s~!=!fA1zOS?!7_Z@b`uqO=x-HjV+LMUm Ly=(IZf4TPyS3tAk diff --git a/Resources/Tests/SharedImages/plane-8-9.glb b/Resources/Tests/SharedImages/plane-8-9.glb deleted file mode 100644 index 469cb3393075aa6e206313ef7b611c2e513e8faa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1508 zcmb_c?TXq!6iv7EJ!F1|m_#cY3dLF%T-0pUE(MV?iIZeXk_nTEyNd{YfIem)q<1EX z-`kebKy>cRoO|xaxe4RN?Tw~sKi_EDJ9zKr?sSW=?~^=dJV(xQi@YKT2!EjD5jm!g zyk|k&AjpYOrq7a$r6d)L=Zqldt!1}#OJq99IW{^I zAyfN2({sYr4t8*^y0k?hNeRb-apa)85PIGSI7=Fl5Lxatn;{iZ;Th1%<~O{-Vcb-g zfMY{=P8l=`*@lUB5U>(MB@9r)n53bIkYlw?$#~Gpv39QMMrOVTL-_^fp_Ejs-Rrat zmAZghRjX=jyL*bYe}nB*^8FXAWdhN0(5_Ek5Rwa}}7*@F7c zqNJ8Mnyv}Vow);|%5UvK0uirCj&|WLjn3B*eHae~R;$us?Ha}wu2gL-k1BkLUx$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?ge|{r zGWH^X@las3svI`1VQk?_-Nx#u##abd@<4(ipjNB(1HPs2M^pHHq20jy5`Isg@9(bL Sa{swAi8wyHwr=p3XTJc9mbGF4 diff --git a/Resources/Tests/SharedImages/plane-9-1.glb b/Resources/Tests/SharedImages/plane-9-1.glb deleted file mode 100644 index 9ea21e3509d3cdea2abbf97aa592d9f5b01117fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1504 zcmb_c>uTFD6!tpy9!0;6<4YDA{~1}9LP`>MNmd3W4B1g^4cju3oVA3&9$*i-N7<2V z=iY6M8Jy@y`p$PQ-*KGIZ?6o)`1!^#Zs5I}1(OZJQAC7bTp<5(gTivPBK(1pZ^*Yz z6h2E5tX6_ZZAL6BSWa>|e=Z2}-#V^mqKK6_9Cu74^)8zAd!ybInJB|g;M7~disrDg z7hCg~!ubmda9mTgcN7nUzPBTAEZbDCZP|OO%GP_Qr0)M91v2Ga5ucH&y;D*s)E|ra zLZSyR<XIfeRGU;e}nDR^8FXAV*}A)(B4VVuF9WgI3`u zQVmLVwWYQdFvF5?iqi^PgnUdSPE#69Ss5oej3e@4POu<-mNI^eBi+r%M(FjwYC&^n zUQsI?&6fn`&fWr1?YHru1dDh>((HmexQo*w!DF_BxkkZ#@8hQdq_AXVn35S?k|pvb zFNqGHh@{TD;54I>u4@xZ$!S^B^||CY7b%unv``(#>{?wL9;cdnD3HKek%F$( z$4$j9BA5;h)~m{4;~J(GuGC$uj%s{`U?qx$Yy6i&DFJ!F1|n2VJf3dLF%T-0pUE(MV?iIZeXk_nTE-9>~xKp(RY(le99 z`?jSt5S=qK-}%nvJBgCT?Tw~sKVNCuTX^s0-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mQqP1oK diff --git a/Resources/Tests/SharedImages/plane-9-3.glb b/Resources/Tests/SharedImages/plane-9-3.glb deleted file mode 100644 index 5f40c7b6e365db2f31169f8b1c45e43b392f27a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 zcmb_c?T*qw6s>D~4>G@-GHsDa|0p7xumQRPZcGR<(=wDXZKs(|aW{m-2k=3C4DU=^ zzE_R0Nn!5HoO|xaxsBr4?Tuj=KVKQfJ9zJ={&7}7lFB1hhQi-KacqT+$kZ^*+Y z3Z5kOSF4;#ZH7F_cuG?_d&(&CUfYgqqL3FU9Jfs*^)8zBy2I`SnJ6KT;M85filwl^ zi=BB&VgCgMIIbvKdy0pC&)pN)7BiiEX$}IPq&4|f$5m?a5=2x;IQQXv* zfMY{N&Nws*ITl7c2w01u5(X$?NYhBJk!QECVmxT&SUcBrqcY!vq5P7FNJ+|Vb=vNs zQWsFWYE_MGy*b4?zrnUE`Th&m#z1r&w0jn`qw;47iD;R6AsCcR(nnbcimfb-RL@vK z%RwoxHr2KQCPY%fNL*sGK#Zs)am>OAFQRn{RoULcS)SB$%rpuu91J={rIU5DJ*#r#B|D*bcsAE z3aZ1W`MSzG6D(npZ7P#UDOgZQWp2^EKOM{l{ul&fqOm_2b%)4pKObELZ~k`dkW<<}s04v~$;Tv3eY{V|CQRE~g*Ll#?Wj zLEY*Tr!vSxm=WzluS&Ny0n9F3sd`x+RrnIY3lSOJq9M=@ip5kFR=qjven)>sr^4lflNA=#22J;@0=70^~Yj9 zlc{~4={ezQ2Rpb>UD~3EWQ1eEIC9Zl1U+wsoTaT;h`eyy?TCuF^bKfb`zzkyC~2!p zz_B5`pbQ#?Y{Nu52v~`s76vF`OtMJC$h90(G9I*YtetDRk(uwoP=0}VBqh~xdfm>U zQWsFGZdHSI-kf6X-(b77eE$V&nLuCe#u2$NC%7O3mN0&c1J%uCBlP-TwxGGQ zsHi25W@`d-XYPQg_S<-nK*Sr8qg}X*lRU-~wuZSz-hKb$ry-=UW~HByIbD-Aazlui zsQ5_{*NNwxrc}^PZDJuf^-CemExPySqs7RZf?{+u^=6a)7`af%#qjgM^JX8um>>b5 zhKMPAgjD;R+Ej)L$Jr@?XJAwnOMzdV3y!lQ!9uBa=@^dQGkPXGRyFugp#rCQ0>YMG zHyL{oz<4OIT2&4k*D$tlrEX(&RO2fID|sNn5Kybt`T^h4_oFHNzR+&qeF?v(&-ZuN SZMpy4nM53)U0XN!%d=mUuC;0a diff --git a/Resources/Tests/SharedImages/plane-9-5.glb b/Resources/Tests/SharedImages/plane-9-5.glb deleted file mode 100644 index 22909852b90854c08f146b388ed637922a7defdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1504 zcmb_c>uTFD6!tpy9!0;6V>=fb{~1}9LP{EUNmd3S4B1g^4cjuZoVJ9(9$*i-N7<2N z=iY6M8Jy@y`p$PQ-*J@8Zm$f(`1#H-Zs5I}`r{44VMvOC@dA1C4GPNDittBDo{(po zD0mUXU#$uvlo_%#XBo-F>?J42Yddb!L?J6PIPRE8s9iMe_eQ-5GEs`3!D({=E1JQ| zUTn={3g<5)uj!JNJcmoj3W=-MbPu>6=&&nEJR*-jYdR8Tm}}jvhfYCag;RF zCE!>SUQh;&Lat?_Ed;E@PzeK+Fd|tbV&pk3TQcsoa;Tk4x{;ahz)*gHc_by(Y;`-$ zeWlKzPSvU!+qyZ%y1&78D*65k*0F);FzDt<(5}p%rZ^%x^^#L4n;_4kG2`i&GgKq+FxSX`=zaP;fE1Rj3=%S>OR_|s z;3ZMvlOnG2&N)q~pzF%SLU0@FMZ#ieEe#I z0)QC;rtl3?-LFfd4HZpmtMr|Lp$nD*zd9EjXGMaA679Tgwau>8wc&B}+(Us1oaPDW zT7KMQ>_rIEp}=aT9oDX4YT-)NMSWD^HG-8qlwb(x2Yg3A49D>M#<+s_E&Luo-``)h S<^F4D5^;ETXyFYu6s~K04>G@-(hCylA4OynHb7UvjR_%UTBhxow$n_fxEn&^1NfjmhG(WN z_f=zTQkXL{-}%nvJB^aX?Tw~sKVNCuJ9zKr-gJv_5Rjr^yg=@9i~KSS34fsE8*)t@ z`A>p)VOS8M%z&ji%Sa{`PdP#EYs+?Y6tFUbreCRrq6(-2Zvv(it%?z-gX< zyyZ7eW>5q$B+7+aRc;#s7+$ziH?umb@fCuVJdj`rtMvoEr|(Bo_!21$@PoM8^ UuiJ9}xig74KD)MV@Rw)50H-0gS^xk5 diff --git a/Resources/Tests/SharedImages/plane-9-7.glb b/Resources/Tests/SharedImages/plane-9-7.glb deleted file mode 100644 index 77b088bfe57108c448c7760fed648c1872a09bc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1512 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 diff --git a/Resources/Tests/SharedImages/texture1.png b/Resources/Tests/SharedImages/texture1.png deleted file mode 100644 index 9594525097353c3fefa248b0cc9732700e301425..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 57706 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 Y$@7zpj^cR82v(Y@vBf8uMvkGs0ipHazyJUM diff --git a/Resources/Tests/SharedImages/tileset.json b/Resources/Tests/SharedImages/tileset.json deleted file mode 100644 index b7569c61b..000000000 --- a/Resources/Tests/SharedImages/tileset.json +++ /dev/null @@ -1,2228 +0,0 @@ -{ - "asset": { - "version": "1.1" - }, - "geometricError": 4096, - "root": { - "boundingVolume": { - "box": [ - 5.4500005, - 0, - 5.4500005, - 5.450000286102295, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 5.450000286102295 - ] - }, - "geometricError": 1024, - "children": [ - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-0-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 1.6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-1-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 2.7, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-2-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 3.8000001999999995, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-3-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 4.9, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-4-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 6, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-5-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 7.1000004, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-6-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 8.2000003, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-7-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 9.3, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-8-9.glb" - } - }, - { - "boundingVolume": { - "box": [ - 0.5, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-0.glb" - } - }, - { - "boundingVolume": { - "box": [ - 1.6, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-1.glb" - } - }, - { - "boundingVolume": { - "box": [ - 2.7, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-2.glb" - } - }, - { - "boundingVolume": { - "box": [ - 3.8000001999999995, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-3.glb" - } - }, - { - "boundingVolume": { - "box": [ - 4.9, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-4.glb" - } - }, - { - "boundingVolume": { - "box": [ - 6, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-5.glb" - } - }, - { - "boundingVolume": { - "box": [ - 7.1000004, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-6.glb" - } - }, - { - "boundingVolume": { - "box": [ - 8.2000003, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-7.glb" - } - }, - { - "boundingVolume": { - "box": [ - 9.3, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-8.glb" - } - }, - { - "boundingVolume": { - "box": [ - 10.400001, - 0, - 10.400001, - 0.5, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0.5 - ] - }, - "geometricError": 512, - "content": { - "uri": "plane-9-9.glb" - } - } - ], - "refine": "ADD" - } -} \ No newline at end of file diff --git a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp index 92a5bf922..eaa00bdc5 100644 --- a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp @@ -27,16 +27,6 @@ IMPLEMENT_SIMPLE_AUTOMATION_TEST( EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter); static void setupForSharedImages(SceneGenerationContext& context) { - static FString Path = - IPluginManager::Get().FindPlugin(TEXT("CesiumForUnreal"))->GetBaseDir(); - // IPluginManager returns a relative path by default - convert it to an - // absolute path. - const FString FullPluginsPath = - IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*Path); - static FString TilesetPath = - TEXT("file://") / FullPluginsPath / - TEXT("Resources/Tests/SharedImages/tileset.json"); - context.setCommonProperties( FVector(21.16677692, -67.38013505, -6375355.1944), FVector(-12, -1300, -5), @@ -56,8 +46,8 @@ static void setupForSharedImages(SceneGenerationContext& context) { georeference->SetOriginPlacement(EOriginPlacement::TrueOrigin); ACesium3DTileset* tileset = context.world->SpawnActor(); - tileset->SetTilesetSource(ETilesetSource::FromUrl); - tileset->SetUrl(TilesetPath); + tileset->SetTilesetSource(ETilesetSource::FromCesiumIon); + tileset->SetIonAssetID(2757071); tileset->SetActorLabel(TEXT("SharedImages")); tileset->SetGeoreference(georeference); From d3fbccce2fed84bcb1624dd7b6a9d3f12d3ddb49 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 08:13:02 +1000 Subject: [PATCH 16/23] AttributeSemantics -> VertexAttributeSemantics --- Source/CesiumRuntime/Private/CesiumGltfTextures.cpp | 4 ++-- Source/CesiumRuntime/Private/CesiumGltfTextures.h | 3 ++- extern/cesium-native | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index a6d93f014..0a8310db4 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -6,8 +6,8 @@ #include "CesiumTextureUtility.h" #include "ExtensionImageCesiumUnreal.h" #include -#include #include +#include #include using namespace CesiumAsync; @@ -177,7 +177,7 @@ bool isValidPrimitive( } auto positionAccessorIt = - primitive.attributes.find(AttributeSemantics::POSITION); + primitive.attributes.find(VertexAttributeSemantics::POSITION); if (positionAccessorIt == primitive.attributes.end()) { // This primitive doesn't have a POSITION semantic, so it's not valid. return false; diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.h b/Source/CesiumRuntime/Private/CesiumGltfTextures.h index d3a7544d1..64540f7e9 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.h +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.h @@ -16,7 +16,8 @@ class CesiumGltfTextures { public: /** * Creates all of the texture resources that are required by the given glTF, - * and adds `ExtensionImageCesiumUnreal` to each. + * and adds `ExtensionImageCesiumUnreal` to each. This is intended to be + * called from a worker thread. */ static CesiumAsync::Future createInWorkerThread( const CesiumAsync::AsyncSystem& asyncSystem, diff --git a/extern/cesium-native b/extern/cesium-native index 82fb7e8fb..e781a9c4b 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 82fb7e8fb38a98163a96df806f5ed21d4f7117e8 +Subproject commit e781a9c4bc4cf67a1108755df847e6bd1636a792 From ddff9db2b5b5cb313f703bdcad201a73fb0eb014 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 18:51:42 +1000 Subject: [PATCH 17/23] Adapt for ImageCesium only design. --- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 2 +- .../Private/CesiumEncodedFeaturesMetadata.cpp | 29 +++++---- .../Private/CesiumEncodedMetadataUtility.cpp | 40 ++++++------ .../Private/CesiumGltfTextures.cpp | 2 +- .../Private/CesiumTextureUtility.cpp | 5 +- .../Private/Tests/Cesium3DTileset.spec.cpp | 2 +- .../Tests/CesiumFeatureIdTexture.spec.cpp | 12 ++-- .../Private/Tests/CesiumGltfSpecUtility.cpp | 11 ++-- .../Private/Tests/CesiumGltfSpecUtility.h | 15 +++-- .../Tests/CesiumTextureUtility.spec.cpp | 64 +++++++++---------- extern/cesium-native | 2 +- 11 files changed, 95 insertions(+), 89 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index c1aa0d900..3a9c3dc6b 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -2045,7 +2045,7 @@ void ACesium3DTileset::updateLastViewUpdateResultState( } if (this->LogAssetStats && this->_pTileset) { - const CesiumGltf::SingleAssetDepot* imageDepot = + const CesiumGltf::SharedAssetDepot* imageDepot = this->_pTileset->getSharedAssetDepot().getImageDepot(); float averageAge; size_t deletionCount; diff --git a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp index 475729dc8..77d1d5b53 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp +++ b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp @@ -143,11 +143,11 @@ std::optional encodeFeatureIdTexture( } // Copy the image, so that we can keep a copy of it in the glTF. - CesiumGltf::SharedAsset imageCopy = - CesiumGltf::SharedAsset(CesiumGltf::ImageCesium(*pFeatureIdImage)); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageCesium(*pFeatureIdImage); encodedFeatureIdTexture.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( - *imageCopy, + *pImageCopy, addressX, addressY, TextureFilter::TF_Nearest, @@ -510,11 +510,12 @@ EncodedPropertyTable encodePropertyTableAnyThreadPart( ? floorSqrtFeatureCount : (floorSqrtFeatureCount + 1); - CesiumGltf::SharedAsset image; - image->width = image->height = textureDimension; - image->bytesPerChannel = encodedFormat.bytesPerChannel; - image->channels = encodedFormat.channels; - image->pixelData.resize( + CesiumUtility::IntrusivePointer pImage = + new CesiumGltf::ImageCesium(); + pImage->width = pImage->height = textureDimension; + pImage->bytesPerChannel = encodedFormat.bytesPerChannel; + pImage->channels = encodedFormat.channels; + pImage->pixelData.resize( textureDimension * textureDimension * encodedFormat.bytesPerChannel * encodedFormat.channels); @@ -523,18 +524,18 @@ EncodedPropertyTable encodePropertyTableAnyThreadPart( CesiumEncodedMetadataParseColorFromString::encode( *pDescription, property, - gsl::span(image->pixelData), + gsl::span(pImage->pixelData), encodedFormat.bytesPerChannel * encodedFormat.channels); } else /* info.Conversion == ECesiumEncodedMetadataConversion::Coerce */ { CesiumEncodedMetadataCoerce::encode( *pDescription, property, - gsl::span(image->pixelData), + gsl::span(pImage->pixelData), encodedFormat.bytesPerChannel * encodedFormat.channels); } encodedProperty.pTexture = loadTextureAnyThreadPart( - *image, + *pImage, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, @@ -657,11 +658,11 @@ EncodedPropertyTexture encodePropertyTextureAnyThreadPart( } // Copy the image, so that we can keep a copy of it in the glTF. - CesiumGltf::SharedAsset imageCopy = - CesiumGltf::SharedAsset(CesiumGltf::ImageCesium(*pImage)); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageCesium(*pImage); encodedProperty.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( - *imageCopy, + *pImageCopy, addressX, addressY, // TODO: account for texture filter diff --git a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp index bc54d4249..f5c8d9b0f 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp @@ -205,19 +205,20 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( ? floorSqrtFeatureCount : (floorSqrtFeatureCount + 1); - CesiumGltf::SharedAsset image; - image->bytesPerChannel = encodedFormat.bytesPerChannel; - image->channels = encodedFormat.channels; - image->compressedPixelFormat = CesiumGltf::GpuCompressedPixelFormat::NONE; - image->height = image->width = ceilSqrtFeatureCount; - image->pixelData.resize(size_t( - image->width * image->height * image->channels * - image->bytesPerChannel)); + CesiumUtility::IntrusivePointer pImage = + new CesiumGltf::ImageCesium(); + pImage->bytesPerChannel = encodedFormat.bytesPerChannel; + pImage->channels = encodedFormat.channels; + pImage->compressedPixelFormat = CesiumGltf::GpuCompressedPixelFormat::NONE; + pImage->height = pImage->width = ceilSqrtFeatureCount; + pImage->pixelData.resize(size_t( + pImage->width * pImage->height * pImage->channels * + pImage->bytesPerChannel)); if (isArray) { switch (gpuType) { case ECesiumMetadataPackedGpuType_DEPRECATED::Uint8_DEPRECATED: { - uint8* pWritePos = reinterpret_cast(image->pixelData.data()); + uint8* pWritePos = reinterpret_cast(pImage->pixelData.data()); int64_t pixelSize = encodedFormat.channels * encodedFormat.bytesPerChannel; for (int64 i = 0; i < featureCount; ++i) { @@ -233,7 +234,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( } } break; case ECesiumMetadataPackedGpuType_DEPRECATED::Float_DEPRECATED: { - uint8* pWritePos = reinterpret_cast(image->pixelData.data()); + uint8* pWritePos = reinterpret_cast(pImage->pixelData.data()); int64_t pixelSize = encodedFormat.channels * encodedFormat.bytesPerChannel; for (int64 i = 0; i < featureCount; ++i) { @@ -257,7 +258,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( } else { switch (gpuType) { case ECesiumMetadataPackedGpuType_DEPRECATED::Uint8_DEPRECATED: { - uint8* pWritePos = reinterpret_cast(image->pixelData.data()); + uint8* pWritePos = reinterpret_cast(pImage->pixelData.data()); for (int64 i = 0; i < featureCount; ++i) { *pWritePos = UCesiumPropertyTablePropertyBlueprintLibrary::GetByte( property, @@ -266,7 +267,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( } } break; case ECesiumMetadataPackedGpuType_DEPRECATED::Float_DEPRECATED: { - float* pWritePosF = reinterpret_cast(image->pixelData.data()); + float* pWritePosF = reinterpret_cast(pImage->pixelData.data()); for (int64 i = 0; i < featureCount; ++i) { *pWritePosF = UCesiumPropertyTablePropertyBlueprintLibrary::GetFloat( property, @@ -278,7 +279,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( } encodedProperty.pTexture = loadTextureAnyThreadPart( - *image, + *pImage, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, @@ -410,11 +411,11 @@ EncodedFeatureTexture encodeFeatureTextureAnyThreadPart( if (pMappedUnrealImageIt) { encodedFeatureTextureProperty.pTexture = pMappedUnrealImageIt->Pin(); } else { - CesiumGltf::SharedAsset imageCopy = - CesiumGltf::SharedAsset(CesiumGltf::ImageCesium(*pImage)); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageCesium(*pImage); encodedFeatureTextureProperty.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( - *imageCopy, + *pImageCopy, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, @@ -516,12 +517,11 @@ EncodedMetadataPrimitive encodeMetadataPrimitiveAnyThreadPart( if (pMappedUnrealImageIt) { encodedFeatureIdTexture.pTexture = pMappedUnrealImageIt->Pin(); } else { - CesiumGltf::SharedAsset imageCopy = - CesiumGltf::SharedAsset( - CesiumGltf::ImageCesium(*pFeatureIdImage)); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageCesium(*pFeatureIdImage); encodedFeatureIdTexture.pTexture = MakeShared( std::move(*loadTextureAnyThreadPart( - *imageCopy, + *pImageCopy, TextureAddress::TA_Clamp, TextureAddress::TA_Clamp, TextureFilter::TF_Nearest, diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index 0a8310db4..2101c8790 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -237,7 +237,7 @@ SharedFuture createTextureInLoadThread( const ExtensionImageCesiumUnreal& extension = ExtensionImageCesiumUnreal::getOrCreate( asyncSystem, - *pImage->cesium, + *pImage->pCesium, sRGB, needsMips, std::nullopt); diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index 6d2a7d0e8..9c0ba9b00 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -304,7 +304,10 @@ TUniquePtr loadTextureFromModelAnyThreadPart( model.getSafe(model.samplers, texture.sampler); TUniquePtr result = - loadTextureFromImageAndSamplerAnyThreadPart(*image.cesium, sampler, sRGB); + loadTextureFromImageAndSamplerAnyThreadPart( + *image.pCesium, + sampler, + sRGB); if (result) { extension.pTexture = result->pTexture; diff --git a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp index eaa00bdc5..03773bb09 100644 --- a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp @@ -71,7 +71,7 @@ static void setupForSharedImages(SceneGenerationContext& context) { void tilesetPass( SceneGenerationContext& context, TestPass::TestingParameter parameter) { - CesiumGltf::SharedAssetDepot& assetDepot = + CesiumGltf::SharedAssetDepots& assetDepot = context.tilesets[0]->GetTileset()->getSharedAssetDepot(); assert(assetDepot.getImagesCount() == 2); } diff --git a/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp index 50d6e20d0..8d01319bf 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp @@ -136,9 +136,9 @@ void FCesiumFeatureIdTextureSpec::Define() { It("constructs valid instance for texture with nonexistent texcoord attribute", [this]() { Image& image = model.images.emplace_back(); - image.cesium->width = image.cesium->height = 1; - image.cesium->channels = 1; - image.cesium->pixelData.push_back(std::byte(42)); + image.pCesium->width = image.pCesium->height = 1; + image.pCesium->channels = 1; + image.pCesium->pixelData.push_back(std::byte(42)); Sampler& sampler = model.samplers.emplace_back(); sampler.wrapS = Sampler::WrapS::CLAMP_TO_EDGE; @@ -175,9 +175,9 @@ void FCesiumFeatureIdTextureSpec::Define() { It("constructs valid instance for texture with invalid texcoord accessor", [this]() { Image& image = model.images.emplace_back(); - image.cesium->width = image.cesium->height = 1; - image.cesium->channels = 1; - image.cesium->pixelData.push_back(std::byte(42)); + image.pCesium->width = image.pCesium->height = 1; + image.pCesium->channels = 1; + image.pCesium->pixelData.push_back(std::byte(42)); Sampler& sampler = model.samplers.emplace_back(); sampler.wrapS = Sampler::WrapS::CLAMP_TO_EDGE; diff --git a/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.cpp b/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.cpp index 47cb9a9d7..db6ded13f 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.cpp @@ -70,12 +70,13 @@ CesiumGltf::FeatureId& AddFeatureIDsAsTextureToModel( const int32_t samplerWrapS, const int32_t samplerWrapT) { CesiumGltf::Image& image = model.images.emplace_back(); - image.cesium->bytesPerChannel = 1; - image.cesium->channels = 1; - image.cesium->width = imageWidth; - image.cesium->height = imageHeight; + image.pCesium.emplace(); + image.pCesium->bytesPerChannel = 1; + image.pCesium->channels = 1; + image.pCesium->width = imageWidth; + image.pCesium->height = imageHeight; - std::vector& data = image.cesium->pixelData; + std::vector& data = image.pCesium->pixelData; data.resize(imageWidth * imageHeight); std::memcpy(data.data(), featureIDs.data(), data.size()); diff --git a/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.h b/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.h index 325b85c25..270b01992 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.h +++ b/Source/CesiumRuntime/Private/Tests/CesiumGltfSpecUtility.h @@ -200,15 +200,16 @@ CesiumGltf::PropertyTextureProperty& AddPropertyTexturePropertyToModel( classProperty.componentType = componentType; CesiumGltf::Image& image = model.images.emplace_back(); - image.cesium->width = 2; - image.cesium->height = 2; - image.cesium->channels = sizeof(T); - image.cesium->bytesPerChannel = 1; - image.cesium->pixelData.resize(values.size() * sizeof(T)); + image.pCesium.emplace(); + image.pCesium->width = 2; + image.pCesium->height = 2; + image.pCesium->channels = sizeof(T); + image.pCesium->bytesPerChannel = 1; + image.pCesium->pixelData.resize(values.size() * sizeof(T)); std::memcpy( - image.cesium->pixelData.data(), + image.pCesium->pixelData.data(), values.data(), - image.cesium->pixelData.size()); + image.pCesium->pixelData.size()); CesiumGltf::Sampler& sampler = model.samplers.emplace_back(); sampler.wrapS = CesiumGltf::Sampler::WrapS::CLAMP_TO_EDGE; diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp index 2fd0b54bf..975275d66 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp @@ -21,7 +21,7 @@ BEGIN_DEFINE_SPEC( std::vector originalPixels; std::vector originalMipPixels; std::vector expectedMipPixelsIfGenerated; -SharedAsset imageCesium; +CesiumUtility::IntrusivePointer pImageCesium; void RunTests(); @@ -51,33 +51,33 @@ void CesiumTextureUtilitySpec::Define() { 0x24, 0x44, 0x84, 0xF4, 0x25, 0x45, 0x85, 0xF5}; originalMipPixels.clear(); - ImageCesium image{}; - imageCesium = SharedAsset(image); - imageCesium->width = 3; - imageCesium->height = 2; + pImageCesium.emplace(); + pImageCesium->width = 3; + pImageCesium->height = 2; TestEqual( "image buffer size is correct", originalPixels.size(), - imageCesium->width * imageCesium->height * - imageCesium->bytesPerChannel * imageCesium->channels); - imageCesium->pixelData.resize(originalPixels.size()); + pImageCesium->width * pImageCesium->height * + pImageCesium->bytesPerChannel * pImageCesium->channels); + pImageCesium->pixelData.resize(originalPixels.size()); std::memcpy( - imageCesium->pixelData.data(), + pImageCesium->pixelData.data(), originalPixels.data(), originalPixels.size()); - ImageCesium copy = *imageCesium; - CesiumGltfReader::GltfReader::generateMipMaps(copy); + CesiumUtility::IntrusivePointer pCopy = + new ImageCesium(*pImageCesium); + CesiumGltfReader::GltfReader::generateMipMaps(*pCopy); expectedMipPixelsIfGenerated.clear(); - if (copy.mipPositions.size() >= 2) { - expectedMipPixelsIfGenerated.resize(copy.mipPositions[1].byteSize); - for (size_t iSrc = copy.mipPositions[1].byteOffset, iDest = 0; - iDest < copy.mipPositions[1].byteSize; + if (pCopy->mipPositions.size() >= 2) { + expectedMipPixelsIfGenerated.resize(pCopy->mipPositions[1].byteSize); + for (size_t iSrc = pCopy->mipPositions[1].byteOffset, iDest = 0; + iDest < pCopy->mipPositions[1].byteSize; ++iSrc, ++iDest) { - expectedMipPixelsIfGenerated[iDest] = uint8_t(copy.pixelData[iSrc]); + expectedMipPixelsIfGenerated[iDest] = uint8_t(pCopy->pixelData[iSrc]); } } }); @@ -87,31 +87,31 @@ void CesiumTextureUtilitySpec::Define() { Describe("With Mips", [this]() { BeforeEach([this]() { - imageCesium = {}; - imageCesium->width = 3; - imageCesium->height = 2; + pImageCesium.emplace(); + pImageCesium->width = 3; + pImageCesium->height = 2; // Original image (3x2) originalPixels = {0x20, 0x40, 0x80, 0xF0, 0x21, 0x41, 0x81, 0xF1, 0x22, 0x42, 0x82, 0xF2, 0x23, 0x43, 0x83, 0xF3, 0x24, 0x44, 0x84, 0xF4, 0x25, 0x45, 0x85, 0xF5}; - imageCesium->mipPositions.emplace_back( + pImageCesium->mipPositions.emplace_back( ImageCesiumMipPosition{0, originalPixels.size()}); // Mip 1 (1x1) originalMipPixels = {0x26, 0x46, 0x86, 0xF6}; - imageCesium->mipPositions.emplace_back(ImageCesiumMipPosition{ - imageCesium->mipPositions[0].byteSize, + pImageCesium->mipPositions.emplace_back(ImageCesiumMipPosition{ + pImageCesium->mipPositions[0].byteSize, originalMipPixels.size()}); - imageCesium->pixelData.resize( + pImageCesium->pixelData.resize( originalPixels.size() + originalMipPixels.size()); std::memcpy( - imageCesium->pixelData.data(), + pImageCesium->pixelData.data(), originalPixels.data(), originalPixels.size()); std::memcpy( - imageCesium->pixelData.data() + originalPixels.size(), + pImageCesium->pixelData.data() + originalPixels.size(), originalMipPixels.data(), originalMipPixels.size()); }); @@ -123,7 +123,7 @@ void CesiumTextureUtilitySpec::Define() { void CesiumTextureUtilitySpec::RunTests() { It("ImageCesium non-sRGB", [this]() { TUniquePtr pHalfLoaded = loadTextureAnyThreadPart( - *imageCesium, + *pImageCesium, TextureAddress::TA_Mirror, TextureAddress::TA_Wrap, TextureFilter::TF_Bilinear, @@ -147,7 +147,7 @@ void CesiumTextureUtilitySpec::RunTests() { It("ImageCesium sRGB", [this]() { TUniquePtr pHalfLoaded = loadTextureAnyThreadPart( - *imageCesium, + *pImageCesium, TextureAddress::TA_Clamp, TextureAddress::TA_Mirror, TextureFilter::TF_Trilinear, @@ -178,7 +178,7 @@ void CesiumTextureUtilitySpec::RunTests() { TUniquePtr pHalfLoaded = loadTextureFromImageAndSamplerAnyThreadPart( - *imageCesium, + *pImageCesium, sampler, false); TestNotNull("pHalfLoaded", pHalfLoaded.Get()); @@ -199,7 +199,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.cesium = imageCesium; + image.pCesium = pImageCesium; Sampler& sampler = model.samplers.emplace_back(); sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; @@ -232,7 +232,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.cesium = imageCesium; + image.pCesium = pImageCesium; Sampler& sampler1 = model.samplers.emplace_back(); sampler1.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; @@ -299,7 +299,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.cesium = imageCesium; + image.pCesium = pImageCesium; Sampler& sampler = model.samplers.emplace_back(); sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; @@ -348,7 +348,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.cesium = imageCesium; + image.pCesium = pImageCesium; Sampler& sampler = model.samplers.emplace_back(); sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; diff --git a/extern/cesium-native b/extern/cesium-native index e781a9c4b..ab21441ce 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit e781a9c4bc4cf67a1108755df847e6bd1636a792 +Subproject commit ab21441ce42bc508c99f0badd6a50612bb5995c8 From edd822680345fa246e7b4cd1776a6d828fb8505a Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Fri, 4 Oct 2024 21:16:58 +1000 Subject: [PATCH 18/23] More updates for changes in Native. --- Source/CesiumRuntime/Private/Cesium3DTileset.cpp | 12 ++++++------ .../Private/Tests/Cesium3DTileset.spec.cpp | 4 ++-- .../Private/Tests/CesiumFeatureIdTexture.spec.cpp | 2 ++ extern/cesium-native | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 3a9c3dc6b..6b50d9d61 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -2045,18 +2045,18 @@ void ACesium3DTileset::updateLastViewUpdateResultState( } if (this->LogAssetStats && this->_pTileset) { - const CesiumGltf::SharedAssetDepot* imageDepot = - this->_pTileset->getSharedAssetDepot().getImageDepot(); + const CesiumGltf::SharedAssetDepot& imageDepot = + this->_pTileset->getSharedAssetSystem().image(); float averageAge; size_t deletionCount; - imageDepot->getDeletionStats(averageAge, deletionCount); + imageDepot.getDeletionStats(averageAge, deletionCount); UE_LOG( LogCesium, Display, TEXT( "Images depot: %d distinct assets, %d total usages, %d assets pending deletion, %f average age"), - imageDepot->getDistinctCount(), - imageDepot->getUsageCount(), + imageDepot.getDistinctCount(), + imageDepot.getUsageCount(), deletionCount, averageAge); } @@ -2235,7 +2235,7 @@ void ACesium3DTileset::Tick(float DeltaTime) { this->UpdateLoadStatus(); if (this->_pTileset) { - this->_pTileset->getSharedAssetDepot().deletionTick(); + this->_pTileset->getSharedAssetSystem().deletionTick(); } } diff --git a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp index 03773bb09..bfa843276 100644 --- a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp @@ -71,8 +71,8 @@ static void setupForSharedImages(SceneGenerationContext& context) { void tilesetPass( SceneGenerationContext& context, TestPass::TestingParameter parameter) { - CesiumGltf::SharedAssetDepots& assetDepot = - context.tilesets[0]->GetTileset()->getSharedAssetDepot(); + CesiumGltf::SharedAssetSystem& assetSystem = + context.tilesets[0]->GetTileset()->getSharedAssetSystem(); assert(assetDepot.getImagesCount() == 2); } diff --git a/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp index 8d01319bf..36da73f40 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumFeatureIdTexture.spec.cpp @@ -136,6 +136,7 @@ void FCesiumFeatureIdTextureSpec::Define() { It("constructs valid instance for texture with nonexistent texcoord attribute", [this]() { Image& image = model.images.emplace_back(); + image.pCesium.emplace(); image.pCesium->width = image.pCesium->height = 1; image.pCesium->channels = 1; image.pCesium->pixelData.push_back(std::byte(42)); @@ -175,6 +176,7 @@ void FCesiumFeatureIdTextureSpec::Define() { It("constructs valid instance for texture with invalid texcoord accessor", [this]() { Image& image = model.images.emplace_back(); + image.pCesium.emplace(); image.pCesium->width = image.pCesium->height = 1; image.pCesium->channels = 1; image.pCesium->pixelData.push_back(std::byte(42)); diff --git a/extern/cesium-native b/extern/cesium-native index ab21441ce..275bac032 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit ab21441ce42bc508c99f0badd6a50612bb5995c8 +Subproject commit 275bac03251ccb1e01d419e402036d97893a77e0 From 0a14600737b3264b1dc0dd0d9273265eb7927268 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 4 Oct 2024 13:50:42 -0400 Subject: [PATCH 19/23] Update cesium-native to shared-assets branch --- extern/cesium-native | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/cesium-native b/extern/cesium-native index 275bac032..aa81cbd67 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 275bac03251ccb1e01d419e402036d97893a77e0 +Subproject commit aa81cbd679db78d166fd2bb0fbfab91f61ed683d From 90ea7e470fe3f840a7caa76ea61b500a20c96b4b Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Fri, 4 Oct 2024 15:13:12 -0400 Subject: [PATCH 20/23] Integrate deletion rework --- Source/CesiumRuntime/Private/Cesium3DTileset.cpp | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 6b50d9d61..e73c2b4f3 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -2047,18 +2047,15 @@ void ACesium3DTileset::updateLastViewUpdateResultState( if (this->LogAssetStats && this->_pTileset) { const CesiumGltf::SharedAssetDepot& imageDepot = this->_pTileset->getSharedAssetSystem().image(); - float averageAge; - size_t deletionCount; - imageDepot.getDeletionStats(averageAge, deletionCount); UE_LOG( LogCesium, Display, TEXT( - "Images depot: %d distinct assets, %d total usages, %d assets pending deletion, %f average age"), + "Images depot: %d distinct assets, %d total usages, %d assets pending deletion, %d total size in bytes"), imageDepot.getDistinctCount(), imageDepot.getUsageCount(), - deletionCount, - averageAge); + imageDepot.getDeletionCandidateCount(), + imageDepot.getDeletionCandidateTotalSizeBytes()); } } } @@ -2233,10 +2230,6 @@ void ACesium3DTileset::Tick(float DeltaTime) { } this->UpdateLoadStatus(); - - if (this->_pTileset) { - this->_pTileset->getSharedAssetSystem().deletionTick(); - } } void ACesium3DTileset::EndPlay(const EEndPlayReason::Type EndPlayReason) { From 9a6328278ae7a93389ad7986524ed7b0a2a64a28 Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Tue, 8 Oct 2024 15:28:18 -0400 Subject: [PATCH 21/23] Add skeleton of a Snowdon test, properly report texture stats --- .../Private/CesiumTextureResource.cpp | 97 ++++++++++++------- .../Private/CesiumTextureResource.h | 4 +- .../Private/CesiumTextureUtility.h | 4 + .../Private/Tests/Cesium3DTileset.spec.cpp | 45 +++++++++ extern/cesium-native | 2 +- 5 files changed, 117 insertions(+), 35 deletions(-) diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index 7fa9cd680..ae9438bfd 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp @@ -31,7 +31,8 @@ class FCesiumUseExistingTextureResource : public FCesiumTextureResource { TextureAddress addressY, bool sRGB, bool useMipsIfAvailable, - uint32 extData); + uint32 extData, + bool isPrimary); FCesiumUseExistingTextureResource( const TSharedPtr& pExistingTexture, @@ -44,7 +45,8 @@ class FCesiumUseExistingTextureResource : public FCesiumTextureResource { TextureAddress addressY, bool sRGB, bool useMipsIfAvailable, - uint32 extData); + uint32 extData, + bool isPrimary); protected: virtual FTextureRHIRef InitializeTextureRHI() override; @@ -182,6 +184,7 @@ FTexture2DRHIRef createAsyncTextureAndWait( ETextureCreateFlags Flags, void** InitialMipData, uint32 NumInitialMips) { + #if ENGINE_VERSION_5_4_OR_HIGHER FGraphEventRef CompletionEvent; @@ -352,6 +355,8 @@ void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { FTexture2DRHIRef textureReference = CreateRHITexture2D_Async(imageCesium, *maybePixelFormat, sRGB); + textureReference->SetName( + FName(UTF8_TO_TCHAR(imageCesium.getUniqueAssetId().c_str()))); auto pResult = TUniquePtr< FCesiumUseExistingTextureResource, FCesiumTextureResourceDeleter>(new FCesiumUseExistingTextureResource( @@ -365,7 +370,8 @@ void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { addressY, sRGB, needsMipMaps, - 0)); + 0, + true)); // Clear the now-unnecessary copy of the pixel data. // Calling clear() isn't good enough because it @@ -421,7 +427,8 @@ FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateWrapped( addressY, sRGB, useMipMapsIfAvailable, - 0)); + 0, + false)); } /*static*/ void FCesiumTextureResource::Destroy(FCesiumTextureResource* p) { @@ -445,7 +452,8 @@ FCesiumTextureResource::FCesiumTextureResource( TextureAddress addressY, bool sRGB, bool useMipsIfAvailable, - uint32 extData) + uint32 extData, + bool isPrimary) : _textureGroup(textureGroup), _width(width), _height(height), @@ -454,7 +462,8 @@ FCesiumTextureResource::FCesiumTextureResource( _addressX(convertAddressMode(addressX)), _addressY(convertAddressMode(addressY)), _useMipsIfAvailable(useMipsIfAvailable), - _platformExtData(extData) { + _platformExtData(extData), + _isPrimary(isPrimary) { this->bGreyScaleFormat = (_format == PF_G8) || (_format == PF_BC4); this->bSRGB = sRGB; STAT(this->_lodGroupStatName = TextureGroupStatFNames[this->_textureGroup]); @@ -500,32 +509,42 @@ void FCesiumTextureResource::InitRHI() { RHIUpdateTextureReference(TextureReferenceRHI, this->TextureRHI); #if STATS - ETextureCreateFlags textureFlags = TexCreate_ShaderResource; - if (this->bSRGB) { - textureFlags |= TexCreate_SRGB; - } - - const FIntPoint MipExtents = - CalcMipMapExtent(this->_width, this->_height, this->_format, 0); - uint32 alignment; - this->_textureSize = RHICalcTexture2DPlatformSize( - MipExtents.X, - MipExtents.Y, - this->_format, - this->GetCurrentMipCount(), - 1, - textureFlags, - FRHIResourceCreateInfo(this->_platformExtData), - alignment); + if (this->_isPrimary) { + ETextureCreateFlags textureFlags = TexCreate_ShaderResource; + if (this->bSRGB) { + textureFlags |= TexCreate_SRGB; + } - INC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); - INC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); + const FIntPoint MipExtents = + CalcMipMapExtent(this->_width, this->_height, this->_format, 0); + const FRHIResourceCreateInfo CreateInfo(this->_platformExtData); + + FDynamicRHI::FRHICalcTextureSizeResult result = RHICalcTexturePlatformSize( + FRHITextureDesc::Create2D( + MipExtents, + this->_format, + CreateInfo.ClearValueBinding, + textureFlags, + this->GetCurrentMipCount(), + 1, + CreateInfo.ExtData), + 0); + + this->_textureSize = result.Size; + + INC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); + INC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); + } #endif } void FCesiumTextureResource::ReleaseRHI() { - DEC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); - DEC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); +#if STATS + if (this->_isPrimary) { + DEC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); + DEC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); + } +#endif RHIUpdateTextureReference(TextureReferenceRHI, nullptr); @@ -570,7 +589,8 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( TextureAddress addressY, bool sRGB, bool useMipsIfAvailable, - uint32 extData) + uint32 extData, + bool isPrimary) : FCesiumTextureResource( textureGroup, width, @@ -581,7 +601,8 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( addressY, sRGB, useMipsIfAvailable, - extData), + extData, + isPrimary), _pExistingTexture(nullptr) { this->TextureRHI = std::move(existingTexture); } @@ -597,7 +618,8 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( TextureAddress addressY, bool sRGB, bool useMipsIfAvailable, - uint32 extData) + uint32 extData, + bool isPrimary) : FCesiumTextureResource( textureGroup, width, @@ -608,7 +630,8 @@ FCesiumUseExistingTextureResource::FCesiumUseExistingTextureResource( addressY, sRGB, useMipsIfAvailable, - extData), + extData, + isPrimary), _pExistingTexture(pExistingTexture) {} FTextureRHIRef FCesiumUseExistingTextureResource::InitializeTextureRHI() { @@ -641,11 +664,19 @@ FCesiumCreateNewTextureResource::FCesiumCreateNewTextureResource( addressY, sRGB, useMipsIfAvailable, - extData), + extData, + true), _image(std::move(image)) {} FTextureRHIRef FCesiumCreateNewTextureResource::InitializeTextureRHI() { - FRHIResourceCreateInfo createInfo{TEXT("CesiumTextureUtility")}; + // Use the asset ID as the name of the texture so it will be visible in the + // Render Resource Viewer. + FString debugName = TEXT("CesiumTextureUtility"); + if (!this->_image.getUniqueAssetId().empty()) { + debugName = UTF8_TO_TCHAR(this->_image.getUniqueAssetId().c_str()); + } + + FRHIResourceCreateInfo createInfo{*debugName}; createInfo.BulkData = nullptr; createInfo.ExtData = _platformExtData; diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.h b/Source/CesiumRuntime/Private/CesiumTextureResource.h index cba96bc6b..4cbd8521a 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.h +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.h @@ -90,7 +90,8 @@ class FCesiumTextureResource : public FTextureResource { TextureAddress addressY, bool sRGB, bool useMipsIfAvailable, - uint32 extData); + uint32 extData, + bool isPrimary); uint32 GetSizeX() const override { return this->_width; } uint32 GetSizeY() const override { return this->_height; } @@ -120,4 +121,5 @@ class FCesiumTextureResource : public FTextureResource { uint32 _platformExtData; FName _lodGroupStatName; uint64 _textureSize; + bool _isPrimary; }; diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 97ee85e43..401643d41 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -4,6 +4,7 @@ #include "CesiumGltf/Model.h" #include "CesiumGltf/SharedAssetDepot.h" +#include "CesiumGltf/Texture.h" #include "CesiumMetadataValueType.h" #include "CesiumTextureResource.h" #include "Engine/Texture.h" @@ -201,4 +202,7 @@ std::optional getPixelFormatForImageCesium( const CesiumGltf::ImageCesium& imageCesium, const std::optional overridePixelFormat); +std::optional +getUnrealTextureFromGltfTexture(const CesiumGltf::Texture& texture); + } // namespace CesiumTextureUtility diff --git a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp index bfa843276..23de71875 100644 --- a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp @@ -4,6 +4,7 @@ #include "Cesium3DTileset.h" #include "CesiumGlobeAnchorComponent.h" +#include "CesiumGltfComponent.h" #include "CesiumLoadTestCore.h" #include "CesiumSceneGeneration.h" #include "CesiumSunSky.h" @@ -88,6 +89,50 @@ bool FCesium3DTilesetSharedImages::RunTest(const FString& Parameters) { TEST_SCREEN_HEIGHT); } +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FCesium3DTilesetSnowdonBenchmark, + "Cesium.Performance.3DTileset.SnowdonBenchmark", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::PerfFilter); + +static void setupForSnowdon(SceneGenerationContext& context) { + context.setCommonProperties( + FVector(-79.8867314431, 40.0223377722, 197.1008007424), + FVector(-293.823058, 6736.144397, 2730.501500), + FRotator(-13.400000, -87.799997, 0.000000), + 60.0f); + + context.sunSky->TimeZone = 5.0f; + context.sunSky->UpdateSun(); + + ACesium3DTileset* tileset = context.world->SpawnActor(); + tileset->SetTilesetSource(ETilesetSource::FromCesiumIon); + tileset->SetIonAssetID(2758251); + + tileset->SetActorLabel(TEXT("Snowdon")); + tileset->SuspendUpdate = false; + tileset->LogSelectionStats = true; + context.tilesets.push_back(tileset); + + ADirectionalLight* Light = context.world->SpawnActor(); + Light->SetActorRotation(FQuat::MakeFromEuler(FVector(0, 0, 270))); +} + +void snowdonPass( + SceneGenerationContext& context, + TestPass::TestingParameter parameter) {} + +bool FCesium3DTilesetSnowdonBenchmark::RunTest(const FString& Parameters) { + std::vector testPasses; + testPasses.push_back(TestPass{"Refresh Pass", snowdonPass, nullptr}); + + return RunLoadTest( + GetBeautifiedTestName(), + setupForSnowdon, + testPasses, + TEST_SCREEN_WIDTH, + TEST_SCREEN_HEIGHT); +} + } // namespace Cesium #endif diff --git a/extern/cesium-native b/extern/cesium-native index aa81cbd67..924b000e0 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit aa81cbd679db78d166fd2bb0fbfab91f61ed683d +Subproject commit 924b000e00fc9054d60591bae494c71532a8ec61 From fc60c1e92d7bedf86cf31bacdb8d7f94952b7515 Mon Sep 17 00:00:00 2001 From: Kevin Ring Date: Thu, 10 Oct 2024 22:20:23 +1100 Subject: [PATCH 22/23] Update cesium-native. --- Source/CesiumRuntime/Private/Cesium3DTileset.cpp | 6 +++--- Source/CesiumRuntime/Private/CesiumTextureResource.h | 2 +- Source/CesiumRuntime/Private/CesiumTextureUtility.h | 2 +- Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp | 2 +- extern/cesium-native | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index e73c2b4f3..54d32122a 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -11,6 +11,7 @@ #include "Cesium3DTilesetLoadFailureDetails.h" #include "Cesium3DTilesetRoot.h" #include "CesiumActors.h" +#include "CesiumAsync/SharedAssetDepot.h" #include "CesiumBoundingVolumeComponent.h" #include "CesiumCamera.h" #include "CesiumCameraManager.h" @@ -19,7 +20,6 @@ #include "CesiumGeospatial/GlobeTransforms.h" #include "CesiumGltf/ImageCesium.h" #include "CesiumGltf/Ktx2TranscodeTargets.h" -#include "CesiumGltf/SharedAssetDepot.h" #include "CesiumGltfComponent.h" #include "CesiumGltfPointsSceneProxyUpdater.h" #include "CesiumGltfPrimitiveComponent.h" @@ -2045,8 +2045,8 @@ void ACesium3DTileset::updateLastViewUpdateResultState( } if (this->LogAssetStats && this->_pTileset) { - const CesiumGltf::SharedAssetDepot& imageDepot = - this->_pTileset->getSharedAssetSystem().image(); + const CesiumAsync::SharedAssetDepot& imageDepot = + *this->_pTileset->getSharedAssetSystem().pImage; UE_LOG( LogCesium, Display, diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.h b/Source/CesiumRuntime/Private/CesiumTextureResource.h index 4cbd8521a..567abec86 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.h +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.h @@ -5,8 +5,8 @@ #include "CesiumCommon.h" #include "Engine/Texture.h" #include "TextureResource.h" +#include #include -#include class FCesiumTextureResource; diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index 401643d41..bcb835db4 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -2,8 +2,8 @@ #pragma once +#include "CesiumAsync/SharedAssetDepot.h" #include "CesiumGltf/Model.h" -#include "CesiumGltf/SharedAssetDepot.h" #include "CesiumGltf/Texture.h" #include "CesiumMetadataValueType.h" #include "CesiumTextureResource.h" diff --git a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp index 23de71875..da746bd60 100644 --- a/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/Cesium3DTileset.spec.cpp @@ -72,7 +72,7 @@ static void setupForSharedImages(SceneGenerationContext& context) { void tilesetPass( SceneGenerationContext& context, TestPass::TestingParameter parameter) { - CesiumGltf::SharedAssetSystem& assetSystem = + CesiumGltfReader::GltfSharedAssetSystem& assetSystem = context.tilesets[0]->GetTileset()->getSharedAssetSystem(); assert(assetDepot.getImagesCount() == 2); } diff --git a/extern/cesium-native b/extern/cesium-native index 924b000e0..526e66068 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 924b000e00fc9054d60591bae494c71532a8ec61 +Subproject commit 526e660687d64d98b3f1aea3b8338dafbac0b1db From 1b59597d65d60bffecb94efd9e434e94401247da Mon Sep 17 00:00:00 2001 From: Ashley Rogers Date: Thu, 10 Oct 2024 15:45:04 -0400 Subject: [PATCH 23/23] Rename ImageCesium --- .../CesiumRuntime/Private/Cesium3DTileset.cpp | 12 +-- .../Private/CesiumEncodedFeaturesMetadata.cpp | 24 +++--- .../Private/CesiumEncodedFeaturesMetadata.h | 2 +- .../Private/CesiumEncodedMetadataUtility.cpp | 22 +++--- .../Private/CesiumEncodedMetadataUtility.h | 2 +- .../Private/CesiumGltfTextures.cpp | 6 +- .../Private/CesiumPropertyTextureProperty.cpp | 6 +- .../Private/CesiumTextureResource.cpp | 44 +++++------ .../Private/CesiumTextureResource.h | 8 +- .../Private/CesiumTextureUtility.cpp | 22 +++--- .../Private/CesiumTextureUtility.h | 14 ++-- ...real.cpp => ExtensionImageAssetUnreal.cpp} | 38 +++++---- ...umUnreal.h => ExtensionImageAssetUnreal.h} | 24 +++--- .../CesiumPropertyTextureProperty.spec.cpp | 78 +++++++++---------- .../Tests/CesiumTextureUtility.spec.cpp | 60 +++++++------- .../Public/CesiumPropertyTextureProperty.h | 2 +- extern/cesium-native | 2 +- 17 files changed, 181 insertions(+), 185 deletions(-) rename Source/CesiumRuntime/Private/{ExtensionImageCesiumUnreal.cpp => ExtensionImageAssetUnreal.cpp} (68%) rename Source/CesiumRuntime/Private/{ExtensionImageCesiumUnreal.h => ExtensionImageAssetUnreal.h} (82%) diff --git a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp index 54d32122a..1be3fbf0d 100644 --- a/Source/CesiumRuntime/Private/Cesium3DTileset.cpp +++ b/Source/CesiumRuntime/Private/Cesium3DTileset.cpp @@ -18,7 +18,7 @@ #include "CesiumCommon.h" #include "CesiumCustomVersion.h" #include "CesiumGeospatial/GlobeTransforms.h" -#include "CesiumGltf/ImageCesium.h" +#include "CesiumGltf/ImageAsset.h" #include "CesiumGltf/Ktx2TranscodeTargets.h" #include "CesiumGltfComponent.h" #include "CesiumGltfPointsSceneProxyUpdater.h" @@ -41,7 +41,7 @@ #include "Engine/TextureRenderTarget2D.h" #include "Engine/World.h" #include "EngineUtils.h" -#include "ExtensionImageCesiumUnreal.h" +#include "ExtensionImageAssetUnreal.h" #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" #include "LevelSequenceActor.h" @@ -867,7 +867,7 @@ class UnrealResourcePreparer } virtual void* prepareRasterInLoadThread( - CesiumGltf::ImageCesium& image, + CesiumGltf::ImageAsset& image, const std::any& rendererOptions) override { auto ppOptions = std::any_cast(&rendererOptions); @@ -893,8 +893,8 @@ class UnrealResourcePreparer // TODO: sRGB should probably be configurable on the raster overlay. bool sRGB = true; - const ExtensionImageCesiumUnreal& extension = - ExtensionImageCesiumUnreal::getOrCreate( + const ExtensionImageAssetUnreal& extension = + ExtensionImageAssetUnreal::getOrCreate( CesiumAsync::AsyncSystem(nullptr), // TODO image, sRGB, @@ -2045,7 +2045,7 @@ void ACesium3DTileset::updateLastViewUpdateResultState( } if (this->LogAssetStats && this->_pTileset) { - const CesiumAsync::SharedAssetDepot& imageDepot = + const CesiumAsync::SharedAssetDepot& imageDepot = *this->_pTileset->getSharedAssetSystem().pImage; UE_LOG( LogCesium, diff --git a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp index 77d1d5b53..9528d8734 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp +++ b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.cpp @@ -99,7 +99,7 @@ encodeFeatureIdAttribute(const FCesiumFeatureIdAttribute& attribute) { std::optional encodeFeatureIdTexture( const FCesiumFeatureIdTexture& texture, - TMap>& + TMap>& featureIdTextureMap) { const ECesiumFeatureIdTextureStatus status = UCesiumFeatureIdTextureBlueprintLibrary::GetFeatureIDTextureStatus( @@ -114,7 +114,7 @@ std::optional encodeFeatureIdTexture( const CesiumGltf::FeatureIdTextureView& featureIdTextureView = texture.getFeatureIdTextureView(); - const CesiumGltf::ImageCesium* pFeatureIdImage = + const CesiumGltf::ImageAsset* pFeatureIdImage = featureIdTextureView.getImage(); TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodeFeatureIdTexture) @@ -143,8 +143,8 @@ std::optional encodeFeatureIdTexture( } // Copy the image, so that we can keep a copy of it in the glTF. - CesiumUtility::IntrusivePointer pImageCopy = - new CesiumGltf::ImageCesium(*pFeatureIdImage); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageAsset(*pFeatureIdImage); encodedFeatureIdTexture.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( *pImageCopy, @@ -177,7 +177,7 @@ EncodedPrimitiveFeatures encodePrimitiveFeaturesAnyThreadPart( // Not all feature ID sets are necessarily textures, but reserve the max // amount just in case. - TMap> + TMap> featureIdTextureMap; featureIdTextureMap.Reserve(featureIDSetDescriptions.Num()); @@ -510,8 +510,8 @@ EncodedPropertyTable encodePropertyTableAnyThreadPart( ? floorSqrtFeatureCount : (floorSqrtFeatureCount + 1); - CesiumUtility::IntrusivePointer pImage = - new CesiumGltf::ImageCesium(); + CesiumUtility::IntrusivePointer pImage = + new CesiumGltf::ImageAsset(); pImage->width = pImage->height = textureDimension; pImage->bytesPerChannel = encodedFormat.bytesPerChannel; pImage->channels = encodedFormat.channels; @@ -592,7 +592,7 @@ EncodedPropertyTable encodePropertyTableAnyThreadPart( EncodedPropertyTexture encodePropertyTextureAnyThreadPart( const FCesiumPropertyTextureDescription& propertyTextureDescription, const FCesiumPropertyTexture& propertyTexture, - TMap>& + TMap>& propertyTexturePropertyMap) { TRACE_CPUPROFILER_EVENT_SCOPE(Cesium::EncodePropertyTexture) @@ -641,7 +641,7 @@ EncodedPropertyTexture encodePropertyTextureAnyThreadPart( encodedProperty.channels[i] = channels[i]; } - const CesiumGltf::ImageCesium* pImage = property.getImage(); + const CesiumGltf::ImageAsset* pImage = property.getImage(); TWeakPtr* pMappedUnrealImageIt = propertyTexturePropertyMap.Find(pImage); @@ -658,8 +658,8 @@ EncodedPropertyTexture encodePropertyTextureAnyThreadPart( } // Copy the image, so that we can keep a copy of it in the glTF. - CesiumUtility::IntrusivePointer pImageCopy = - new CesiumGltf::ImageCesium(*pImage); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageAsset(*pImage); encodedProperty.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( *pImageCopy, @@ -772,7 +772,7 @@ EncodedModelMetadata encodeModelMetadataAnyThreadPart( UCesiumModelMetadataBlueprintLibrary::GetPropertyTextures(metadata); result.propertyTextures.Reserve(propertyTextures.Num()); - TMap> + TMap> propertyTexturePropertyMap; propertyTexturePropertyMap.Reserve(propertyTextures.Num()); diff --git a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.h b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.h index 31ec20866..0bb838505 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.h +++ b/Source/CesiumRuntime/Private/CesiumEncodedFeaturesMetadata.h @@ -476,7 +476,7 @@ EncodedPropertyTexture encodePropertyTextureAnyThreadPart( const FCesiumPropertyTextureDescription& propertyTextureDescription, const FCesiumPropertyTexture& propertyTexture, TMap< - const CesiumGltf::ImageCesium*, + const CesiumGltf::ImageAsset*, TWeakPtr>& propertyTexturePropertyMap); diff --git a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp index f5c8d9b0f..28f6fa3d1 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.cpp @@ -205,8 +205,8 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( ? floorSqrtFeatureCount : (floorSqrtFeatureCount + 1); - CesiumUtility::IntrusivePointer pImage = - new CesiumGltf::ImageCesium(); + CesiumUtility::IntrusivePointer pImage = + new CesiumGltf::ImageAsset(); pImage->bytesPerChannel = encodedFormat.bytesPerChannel; pImage->channels = encodedFormat.channels; pImage->compressedPixelFormat = CesiumGltf::GpuCompressedPixelFormat::NONE; @@ -293,7 +293,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( } EncodedFeatureTexture encodeFeatureTextureAnyThreadPart( - TMap>& + TMap>& featureTexturePropertyMap, const FFeatureTextureDescription& featureTextureDescription, const FString& featureTextureName, @@ -322,7 +322,7 @@ EncodedFeatureTexture encodeFeatureTextureAnyThreadPart( const FCesiumPropertyTextureProperty& featureTextureProperty = propertyIt.Value; - const CesiumGltf::ImageCesium* pImage = featureTextureProperty.getImage(); + const CesiumGltf::ImageAsset* pImage = featureTextureProperty.getImage(); if (!pImage) { UE_LOG( @@ -411,8 +411,8 @@ EncodedFeatureTexture encodeFeatureTextureAnyThreadPart( if (pMappedUnrealImageIt) { encodedFeatureTextureProperty.pTexture = pMappedUnrealImageIt->Pin(); } else { - CesiumUtility::IntrusivePointer pImageCopy = - new CesiumGltf::ImageCesium(*pImage); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageAsset(*pImage); encodedFeatureTextureProperty.pTexture = MakeShared(std::move(*loadTextureAnyThreadPart( *pImageCopy, @@ -462,7 +462,7 @@ EncodedMetadataPrimitive encodeMetadataPrimitiveAnyThreadPart( } } - TMap> + TMap> featureIdTextureMap; featureIdTextureMap.Reserve(featureIdTextures.Num()); @@ -490,7 +490,7 @@ EncodedMetadataPrimitive encodeMetadataPrimitiveAnyThreadPart( if (pFeatureIdTexture) { const CesiumGltf::FeatureIdTextureView& featureIdTextureView = pFeatureIdTexture->getFeatureIdTextureView(); - const CesiumGltf::ImageCesium* pFeatureIdImage = + const CesiumGltf::ImageAsset* pFeatureIdImage = featureIdTextureView.getImage(); if (!pFeatureIdImage) { @@ -517,8 +517,8 @@ EncodedMetadataPrimitive encodeMetadataPrimitiveAnyThreadPart( if (pMappedUnrealImageIt) { encodedFeatureIdTexture.pTexture = pMappedUnrealImageIt->Pin(); } else { - CesiumUtility::IntrusivePointer pImageCopy = - new CesiumGltf::ImageCesium(*pFeatureIdImage); + CesiumUtility::IntrusivePointer pImageCopy = + new CesiumGltf::ImageAsset(*pFeatureIdImage); encodedFeatureIdTexture.pTexture = MakeShared( std::move(*loadTextureAnyThreadPart( *pImageCopy, @@ -599,7 +599,7 @@ EncodedMetadata encodeMetadataAnyThreadPart( const TMap& featureTextures = UCesiumModelMetadataBlueprintLibrary::GetFeatureTextures(metadata); result.encodedFeatureTextures.Reserve(featureTextures.Num()); - TMap> + TMap> featureTexturePropertyMap; featureTexturePropertyMap.Reserve(featureTextures.Num()); for (const auto& featureTextureIt : featureTextures) { diff --git a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.h b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.h index 070f2b00c..84c4b3d23 100644 --- a/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.h +++ b/Source/CesiumRuntime/Private/CesiumEncodedMetadataUtility.h @@ -106,7 +106,7 @@ EncodedMetadataFeatureTable encodeMetadataFeatureTableAnyThreadPart( EncodedFeatureTexture encodeFeatureTextureAnyThreadPart( TMap< - const CesiumGltf::ImageCesium*, + const CesiumGltf::ImageAsset*, TWeakPtr>& featureTexturePropertyMap, const FFeatureTextureDescription& featureTextureDescription, diff --git a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp index 2101c8790..bc52d55bf 100644 --- a/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp +++ b/Source/CesiumRuntime/Private/CesiumGltfTextures.cpp @@ -4,7 +4,7 @@ #include "CesiumRuntime.h" #include "CesiumTextureResource.h" #include "CesiumTextureUtility.h" -#include "ExtensionImageCesiumUnreal.h" +#include "ExtensionImageAssetUnreal.h" #include #include #include @@ -234,8 +234,8 @@ SharedFuture createTextureInLoadThread( check(pTexture->source >= 0 && pTexture->source < imageNeedsMipmaps.size()); bool needsMips = imageNeedsMipmaps[pTexture->source]; - const ExtensionImageCesiumUnreal& extension = - ExtensionImageCesiumUnreal::getOrCreate( + const ExtensionImageAssetUnreal& extension = + ExtensionImageAssetUnreal::getOrCreate( asyncSystem, *pImage->pCesium, sRGB, diff --git a/Source/CesiumRuntime/Private/CesiumPropertyTextureProperty.cpp b/Source/CesiumRuntime/Private/CesiumPropertyTextureProperty.cpp index 0cd8653fa..cc96ff84b 100644 --- a/Source/CesiumRuntime/Private/CesiumPropertyTextureProperty.cpp +++ b/Source/CesiumRuntime/Private/CesiumPropertyTextureProperty.cpp @@ -333,13 +333,13 @@ const CesiumGltf::Sampler* FCesiumPropertyTextureProperty::getSampler() const { }); } -const CesiumGltf::ImageCesium* +const CesiumGltf::ImageAsset* FCesiumPropertyTextureProperty::getImage() const { - return propertyTexturePropertyCallback( + return propertyTexturePropertyCallback( this->_property, this->_valueType, this->_normalized, - [](const auto& view) -> const CesiumGltf::ImageCesium* { + [](const auto& view) -> const CesiumGltf::ImageAsset* { return view.getImage(); }); } diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp index ae9438bfd..e86e79986 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.cpp @@ -65,7 +65,7 @@ class FCesiumUseExistingTextureResource : public FCesiumTextureResource { class FCesiumCreateNewTextureResource : public FCesiumTextureResource { public: FCesiumCreateNewTextureResource( - CesiumGltf::ImageCesium&& image, + CesiumGltf::ImageAsset&& image, TextureGroup textureGroup, uint32 width, uint32 height, @@ -81,7 +81,7 @@ class FCesiumCreateNewTextureResource : public FCesiumTextureResource { virtual FTextureRHIRef InitializeTextureRHI() override; private: - CesiumGltf::ImageCesium _image; + CesiumGltf::ImageAsset _image; }; ESamplerFilter convertFilter(TextureFilter filter) { @@ -126,7 +126,7 @@ void CopyMip( void* pDest, uint32 destPitch, EPixelFormat format, - const CesiumGltf::ImageCesium& src, + const CesiumGltf::ImageAsset& src, uint32 mipIndex) { size_t byteOffset = 0; size_t byteSize = 0; @@ -134,7 +134,7 @@ void CopyMip( byteOffset = 0; byteSize = src.pixelData.size(); } else { - const CesiumGltf::ImageCesiumMipPosition& mipPos = + const CesiumGltf::ImageAssetMipPosition& mipPos = src.mipPositions[mipIndex]; byteOffset = mipPos.byteOffset; byteSize = mipPos.byteSize; @@ -245,7 +245,7 @@ FTexture2DRHIRef createAsyncTextureAndWait( * @return The RHI texture reference. */ FTexture2DRHIRef CreateRHITexture2D_Async( - const CesiumGltf::ImageCesium& image, + const CesiumGltf::ImageAsset& image, EPixelFormat format, bool sRGB) { check(GRHISupportsAsyncTextureCreation); @@ -271,7 +271,7 @@ FTexture2DRHIRef CreateRHITexture2D_Async( void* mipsData[16]; for (size_t i = 0; i < mipCount; ++i) { - const CesiumGltf::ImageCesiumMipPosition& mipPos = image.mipPositions[i]; + const CesiumGltf::ImageAssetMipPosition& mipPos = image.mipPositions[i]; mipsData[i] = (void*)(&image.pixelData[mipPos.byteOffset]); } @@ -303,7 +303,7 @@ void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { } /*static*/ FCesiumTextureResourceUniquePtr FCesiumTextureResource::CreateNew( - CesiumGltf::ImageCesium& imageCesium, + CesiumGltf::ImageAsset& imageCesium, TextureGroup textureGroup, const std::optional& overridePixelFormat, TextureFilter filter, @@ -328,7 +328,7 @@ void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { } std::optional maybePixelFormat = - CesiumTextureUtility::getPixelFormatForImageCesium( + CesiumTextureUtility::getPixelFormatForImageAsset( imageCesium, overridePixelFormat); if (!maybePixelFormat) { @@ -379,7 +379,7 @@ void FCesiumTextureResourceDeleter::operator()(FCesiumTextureResource* p) { std::vector pixelData; imageCesium.pixelData.swap(pixelData); - std::vector mipPositions; + std::vector mipPositions; imageCesium.mipPositions.swap(mipPositions); return pResult; @@ -519,18 +519,16 @@ void FCesiumTextureResource::InitRHI() { CalcMipMapExtent(this->_width, this->_height, this->_format, 0); const FRHIResourceCreateInfo CreateInfo(this->_platformExtData); - FDynamicRHI::FRHICalcTextureSizeResult result = RHICalcTexturePlatformSize( - FRHITextureDesc::Create2D( - MipExtents, - this->_format, - CreateInfo.ClearValueBinding, - textureFlags, - this->GetCurrentMipCount(), - 1, - CreateInfo.ExtData), - 0); - - this->_textureSize = result.Size; + uint32 alignment; + this->_textureSize = RHICalcTexture2DPlatformSize( + MipExtents.X, + MipExtents.Y, + this->_format, + this->GetCurrentMipCount(), + 1, + textureFlags, + FRHIResourceCreateInfo(this->_platformExtData), + alignment); INC_DWORD_STAT_BY(STAT_TextureMemory, this->_textureSize); INC_DWORD_STAT_FNAME_BY(this->_lodGroupStatName, this->_textureSize); @@ -643,7 +641,7 @@ FTextureRHIRef FCesiumUseExistingTextureResource::InitializeTextureRHI() { } FCesiumCreateNewTextureResource::FCesiumCreateNewTextureResource( - CesiumGltf::ImageCesium&& image, + CesiumGltf::ImageAsset&& image, TextureGroup textureGroup, uint32 width, uint32 height, @@ -732,7 +730,7 @@ FTextureRHIRef FCesiumCreateNewTextureResource::InitializeTextureRHI() { std::vector pixelData; this->_image.pixelData.swap(pixelData); - std::vector mipPositions; + std::vector mipPositions; this->_image.mipPositions.swap(mipPositions); return rhiTexture; diff --git a/Source/CesiumRuntime/Private/CesiumTextureResource.h b/Source/CesiumRuntime/Private/CesiumTextureResource.h index 567abec86..4e7906c18 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureResource.h +++ b/Source/CesiumRuntime/Private/CesiumTextureResource.h @@ -6,7 +6,7 @@ #include "Engine/Texture.h" #include "TextureResource.h" #include -#include +#include class FCesiumTextureResource; @@ -25,7 +25,7 @@ using FCesiumTextureResourceUniquePtr = class FCesiumTextureResource : public FTextureResource { public: /** - * Create a new FCesiumTextureResource from an ImageCesium and the given + * Create a new FCesiumTextureResource from an `ImageAsset` and the given * sampling parameters. This method is intended to be called from a worker * thread, not from the game or render thread. * @@ -34,7 +34,7 @@ class FCesiumTextureResource : public FTextureResource { * `sizeBytes` will be set to its previous size. * @param textureGroup The texture group in which to create this texture. * @param overridePixelFormat Overrides the pixel format. If std::nullopt, the - * format is inferred from the `ImageCesium`. + * format is inferred from the `ImageAsset`. * @param filter The texture filtering to use when sampling this texture. * @param addressX The X texture addressing mode to use when sampling this * texture. @@ -48,7 +48,7 @@ class FCesiumTextureResource : public FTextureResource { * created. */ static FCesiumTextureResourceUniquePtr CreateNew( - CesiumGltf::ImageCesium& imageCesium, + CesiumGltf::ImageAsset& imageCesium, TextureGroup textureGroup, const std::optional& overridePixelFormat, TextureFilter filter, diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp index 9c0ba9b00..603b6bde9 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.cpp @@ -10,7 +10,7 @@ #include "CesiumTextureResource.h" #include "Containers/ResourceArray.h" #include "DynamicRHI.h" -#include "ExtensionImageCesiumUnreal.h" +#include "ExtensionImageAssetUnreal.h" #include "GenericPlatform/GenericPlatformProcess.h" #include "PixelFormat.h" #include "RHICommandList.h" @@ -23,7 +23,7 @@ #include "UObject/Package.h" #include #include -#include +#include #include #include #include @@ -109,7 +109,7 @@ FTexture2DRHIRef createAsyncTextureAndWaitOld( * @return The RHI texture reference. */ FTexture2DRHIRef CreateRHITexture2D_AsyncOld( - const CesiumGltf::ImageCesium& image, + const CesiumGltf::ImageAsset& image, EPixelFormat format, bool sRGB) { check(GRHISupportsAsyncTextureCreation); @@ -135,7 +135,7 @@ FTexture2DRHIRef CreateRHITexture2D_AsyncOld( void* mipsData[16]; for (size_t i = 0; i < mipCount; ++i) { - const CesiumGltf::ImageCesiumMipPosition& mipPos = image.mipPositions[i]; + const CesiumGltf::ImageAssetMipPosition& mipPos = image.mipPositions[i]; mipsData[i] = (void*)(&image.pixelData[mipPos.byteOffset]); } @@ -369,7 +369,7 @@ bool getUseMipmapsIfAvailableFromSampler(const CesiumGltf::Sampler& sampler) { } TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::ImageCesium& image, + CesiumGltf::ImageAsset& image, const CesiumGltf::Sampler& sampler, bool sRGB) { return loadTextureAnyThreadPart( @@ -414,7 +414,7 @@ static UTexture2D* CreateTexture2D(LoadedTextureResult* pHalfLoadedTexture) { } TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::ImageCesium& image, + CesiumGltf::ImageAsset& image, TextureAddress addressX, TextureAddress addressY, TextureFilter filter, @@ -422,10 +422,10 @@ TUniquePtr loadTextureAnyThreadPart( TextureGroup group, bool sRGB, std::optional overridePixelFormat) { - // The FCesiumTextureResource for the ImageCesium should already be created at + // The FCesiumTextureResource for the ImageAsset should already be created at // this point, if it can be. - const ExtensionImageCesiumUnreal& extension = - ExtensionImageCesiumUnreal::getOrCreate( + const ExtensionImageAssetUnreal& extension = + ExtensionImageAssetUnreal::getOrCreate( CesiumAsync::AsyncSystem(nullptr), image, sRGB, @@ -546,8 +546,8 @@ TextureAddress convertGltfWrapTToUnreal(int32_t wrapT) { } } -std::optional getPixelFormatForImageCesium( - const ImageCesium& imageCesium, +std::optional getPixelFormatForImageAsset( + const ImageAsset& imageCesium, const std::optional overridePixelFormat) { if (imageCesium.compressedPixelFormat != GpuCompressedPixelFormat::NONE) { switch (imageCesium.compressedPixelFormat) { diff --git a/Source/CesiumRuntime/Private/CesiumTextureUtility.h b/Source/CesiumRuntime/Private/CesiumTextureUtility.h index bcb835db4..3c1b9d8e5 100644 --- a/Source/CesiumRuntime/Private/CesiumTextureUtility.h +++ b/Source/CesiumRuntime/Private/CesiumTextureUtility.h @@ -17,7 +17,7 @@ #include namespace CesiumGltf { -struct ImageCesium; +struct ImageAsset; struct Texture; } // namespace CesiumGltf @@ -118,15 +118,15 @@ TUniquePtr loadTextureFromModelAnyThreadPart( * and can be empty. */ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( - CesiumGltf::ImageCesium& image, + CesiumGltf::ImageAsset& image, const CesiumGltf::Sampler& sampler, bool sRGB); /** * @brief Does the asynchronous part of renderer resource preparation for * a texture.The given image _must_ be prepared before calling this method by - * calling {@link ExtensionImageCesiumUnreal::getOrCreate} and then waiting - * for {@link ExtensionImageCesiumUnreal::getFuture} to resolve. This method + * calling {@link ExtensionImageAssetUnreal::getOrCreate} and then waiting + * for {@link ExtensionImageAssetUnreal::getFuture} to resolve. This method * should be called in a background thread. * * @param imageCesium The image. @@ -142,7 +142,7 @@ TUniquePtr loadTextureFromImageAndSamplerAnyThreadPart( * @return The loaded texture. */ TUniquePtr loadTextureAnyThreadPart( - CesiumGltf::ImageCesium& image, + CesiumGltf::ImageAsset& image, TextureAddress addressX, TextureAddress addressY, TextureFilter filter, @@ -198,8 +198,8 @@ TextureAddress convertGltfWrapSToUnreal(int32_t wrapS); */ TextureAddress convertGltfWrapTToUnreal(int32_t wrapT); -std::optional getPixelFormatForImageCesium( - const CesiumGltf::ImageCesium& imageCesium, +std::optional getPixelFormatForImageAsset( + const CesiumGltf::ImageAsset& imageCesium, const std::optional overridePixelFormat); std::optional diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp b/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp similarity index 68% rename from Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp rename to Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp index f1081af7e..9f2120a9b 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.cpp +++ b/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.cpp @@ -1,7 +1,7 @@ -#include "ExtensionImageCesiumUnreal.h" +#include "ExtensionImageAssetUnreal.h" #include "CesiumRuntime.h" #include "CesiumTextureUtility.h" -#include +#include #include using namespace CesiumAsync; @@ -12,17 +12,15 @@ namespace { std::mutex createExtensionMutex; -std::pair>> -getOrCreateImageFuture( - const AsyncSystem& asyncSystem, - ImageCesium& imageCesium); +std::pair>> +getOrCreateImageFuture(const AsyncSystem& asyncSystem, ImageAsset& imageCesium); } // namespace -/*static*/ const ExtensionImageCesiumUnreal& -ExtensionImageCesiumUnreal::getOrCreate( +/*static*/ const ExtensionImageAssetUnreal& +ExtensionImageAssetUnreal::getOrCreate( const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::ImageCesium& imageCesium, + CesiumGltf::ImageAsset& imageCesium, bool sRGB, bool needsMipMaps, const std::optional& overridePixelFormat) { @@ -55,43 +53,43 @@ ExtensionImageCesiumUnreal::getOrCreate( return extension; } -ExtensionImageCesiumUnreal::ExtensionImageCesiumUnreal( +ExtensionImageAssetUnreal::ExtensionImageAssetUnreal( const CesiumAsync::SharedFuture& future) : _pTextureResource(nullptr), _futureCreateResource(future) {} const TSharedPtr& -ExtensionImageCesiumUnreal::getTextureResource() const { +ExtensionImageAssetUnreal::getTextureResource() const { return this->_pTextureResource; } -CesiumAsync::SharedFuture& ExtensionImageCesiumUnreal::getFuture() { +CesiumAsync::SharedFuture& ExtensionImageAssetUnreal::getFuture() { return this->_futureCreateResource; } const CesiumAsync::SharedFuture& -ExtensionImageCesiumUnreal::getFuture() const { +ExtensionImageAssetUnreal::getFuture() const { return this->_futureCreateResource; } namespace { -// Returns the ExtensionImageCesiumUnreal, which is created if it does not +// Returns the ExtensionImageAssetUnreal, which is created if it does not // already exist. It _may_ also return a Promise, in which case the calling // thread is responsible for doing the loading and should resolve the Promise // when it's done. -std::pair>> +std::pair>> getOrCreateImageFuture( const AsyncSystem& asyncSystem, - ImageCesium& imageCesium) { + ImageAsset& imageCesium) { std::scoped_lock lock(createExtensionMutex); - ExtensionImageCesiumUnreal* pExtension = - imageCesium.getExtension(); + ExtensionImageAssetUnreal* pExtension = + imageCesium.getExtension(); if (!pExtension) { // This thread will work on this image. Promise promise = asyncSystem.createPromise(); - ExtensionImageCesiumUnreal& extension = - imageCesium.addExtension( + ExtensionImageAssetUnreal& extension = + imageCesium.addExtension( promise.getFuture().share()); return {extension, std::move(promise)}; } else { diff --git a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h b/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.h similarity index 82% rename from Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h rename to Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.h index 133361c94..824bc4d27 100644 --- a/Source/CesiumRuntime/Private/ExtensionImageCesiumUnreal.h +++ b/Source/CesiumRuntime/Private/ExtensionImageAssetUnreal.h @@ -9,14 +9,14 @@ #include namespace CesiumGltf { -struct ImageCesium; +struct ImageAsset; } /** - * @brief An extension attached to an ImageCesium in order to hold + * @brief An extension attached to an ImageAsset in order to hold * Unreal-specific information about it. * - * ImageCesium instances are shared between multiple textures on a single model, + * ImageAsset instances are shared between multiple textures on a single model, * and even between models in some cases, but we strive to have only one copy of * the image bytes in GPU memory. * @@ -31,29 +31,29 @@ struct ImageCesium; * Because we'll never be sampling from this texture resource, the texture * filtering and addressing parameters have default values. */ -struct ExtensionImageCesiumUnreal { - static inline constexpr const char* TypeName = "ExtensionImageCesiumUnreal"; +struct ExtensionImageAssetUnreal { + static inline constexpr const char* TypeName = "ExtensionImageAssetUnreal"; static inline constexpr const char* ExtensionName = - "PRIVATE_ImageCesium_Unreal"; + "PRIVATE_ImageAsset_Unreal"; /** - * @brief Gets an Unreal texture resource from the given `ImageCesium`, + * @brief Gets an Unreal texture resource from the given `ImageAsset`, * creating it if necessary. * * When this function is called for the first time on a particular - * `ImageCesium`, the asynchronous process to create an Unreal + * `ImageAsset`, the asynchronous process to create an Unreal * `FTextureResource` from it is kicked off. On successive invocations * (perhaps from other threads), the existing instance is returned. It is safe - * to call this method on the same `ImageCesium` instance from multiple + * to call this method on the same `ImageAsset` instance from multiple * threads simultaneously as long as no other thread is modifying the instance * at the same time. * * To determine if the asynchronous `FTextureResource` creation process has * completed, use {@link getFuture}. */ - static const ExtensionImageCesiumUnreal& getOrCreate( + static const ExtensionImageAssetUnreal& getOrCreate( const CesiumAsync::AsyncSystem& asyncSystem, - CesiumGltf::ImageCesium& imageCesium, + CesiumGltf::ImageAsset& imageCesium, bool sRGB, bool needsMipMaps, const std::optional& overridePixelFormat); @@ -64,7 +64,7 @@ struct ExtensionImageCesiumUnreal { * @param future The future that will resolve when loading of the * {@link getTextureResource} is complete. */ - ExtensionImageCesiumUnreal(const CesiumAsync::SharedFuture& future); + ExtensionImageAssetUnreal(const CesiumAsync::SharedFuture& future); /** * Gets the created texture resource. This resource should not be accessed or diff --git a/Source/CesiumRuntime/Private/Tests/CesiumPropertyTextureProperty.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumPropertyTextureProperty.spec.cpp index d9361976c..d20bc855c 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumPropertyTextureProperty.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumPropertyTextureProperty.spec.cpp @@ -81,7 +81,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::UINT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -170,7 +170,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.normalized = true; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -235,7 +235,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.count = 2; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -312,7 +312,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -434,7 +434,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::UINT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -472,7 +472,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT16; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -526,7 +526,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -590,7 +590,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -634,7 +634,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::FLOAT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -688,7 +688,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -758,7 +758,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::FLOAT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -804,7 +804,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::UINT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -848,7 +848,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.scale = scale; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -912,7 +912,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.normalized = true; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -955,7 +955,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::FLOAT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1008,7 +1008,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.scale = scale; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 1; @@ -1066,7 +1066,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -1110,7 +1110,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::FLOAT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1168,7 +1168,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.defaultProperty = {defaultValue[0], defaultValue[1]}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -1239,7 +1239,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.normalized = true; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -1290,7 +1290,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::UINT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -1342,7 +1342,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.scale = {scale[0], scale[1]}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -1409,7 +1409,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 3; @@ -1453,7 +1453,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::FLOAT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1514,7 +1514,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { defaultValue[2]}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 3; @@ -1585,7 +1585,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.normalized = true; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 3; @@ -1637,7 +1637,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 3; @@ -1689,7 +1689,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.scale = {scale[0], scale[1], scale[2]}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 3; @@ -1757,7 +1757,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.normalized = true; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1814,7 +1814,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT8; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1872,7 +1872,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.scale = {scale[0], scale[1], scale[2], scale[3]}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1923,7 +1923,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -1999,7 +1999,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.count = 2; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -2067,7 +2067,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.noData = {0, 0}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -2155,7 +2155,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.defaultProperty = {10, 20}; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 2; @@ -2256,7 +2256,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.componentType = ClassProperty::ComponentType::INT32; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -2316,7 +2316,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.scale = scale; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -2373,7 +2373,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.noData = noData; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; @@ -2441,7 +2441,7 @@ void FCesiumPropertyTexturePropertySpec::Define() { classProperty.defaultProperty = defaultValue; Sampler sampler; - ImageCesium image; + ImageAsset image; image.width = 2; image.height = 2; image.channels = 4; diff --git a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp index 975275d66..c5dbd9862 100644 --- a/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp +++ b/Source/CesiumRuntime/Private/Tests/CesiumTextureUtility.spec.cpp @@ -2,7 +2,7 @@ #include "CesiumTextureUtility.h" #include "CesiumAsync/AsyncSystem.h" -#include "ExtensionImageCesiumUnreal.h" +#include "ExtensionImageAssetUnreal.h" #include "Misc/AutomationTest.h" #include "RenderingThread.h" #include @@ -21,7 +21,7 @@ BEGIN_DEFINE_SPEC( std::vector originalPixels; std::vector originalMipPixels; std::vector expectedMipPixelsIfGenerated; -CesiumUtility::IntrusivePointer pImageCesium; +CesiumUtility::IntrusivePointer pImageAsset; void RunTests(); @@ -51,23 +51,23 @@ void CesiumTextureUtilitySpec::Define() { 0x24, 0x44, 0x84, 0xF4, 0x25, 0x45, 0x85, 0xF5}; originalMipPixels.clear(); - pImageCesium.emplace(); - pImageCesium->width = 3; - pImageCesium->height = 2; + pImageAsset.emplace(); + pImageAsset->width = 3; + pImageAsset->height = 2; TestEqual( "image buffer size is correct", originalPixels.size(), - pImageCesium->width * pImageCesium->height * - pImageCesium->bytesPerChannel * pImageCesium->channels); - pImageCesium->pixelData.resize(originalPixels.size()); + pImageAsset->width * pImageAsset->height * + pImageAsset->bytesPerChannel * pImageAsset->channels); + pImageAsset->pixelData.resize(originalPixels.size()); std::memcpy( - pImageCesium->pixelData.data(), + pImageAsset->pixelData.data(), originalPixels.data(), originalPixels.size()); - CesiumUtility::IntrusivePointer pCopy = - new ImageCesium(*pImageCesium); + CesiumUtility::IntrusivePointer pCopy = + new ImageAsset(*pImageAsset); CesiumGltfReader::GltfReader::generateMipMaps(*pCopy); expectedMipPixelsIfGenerated.clear(); @@ -87,31 +87,31 @@ void CesiumTextureUtilitySpec::Define() { Describe("With Mips", [this]() { BeforeEach([this]() { - pImageCesium.emplace(); - pImageCesium->width = 3; - pImageCesium->height = 2; + pImageAsset.emplace(); + pImageAsset->width = 3; + pImageAsset->height = 2; // Original image (3x2) originalPixels = {0x20, 0x40, 0x80, 0xF0, 0x21, 0x41, 0x81, 0xF1, 0x22, 0x42, 0x82, 0xF2, 0x23, 0x43, 0x83, 0xF3, 0x24, 0x44, 0x84, 0xF4, 0x25, 0x45, 0x85, 0xF5}; - pImageCesium->mipPositions.emplace_back( - ImageCesiumMipPosition{0, originalPixels.size()}); + pImageAsset->mipPositions.emplace_back( + ImageAssetMipPosition{0, originalPixels.size()}); // Mip 1 (1x1) originalMipPixels = {0x26, 0x46, 0x86, 0xF6}; - pImageCesium->mipPositions.emplace_back(ImageCesiumMipPosition{ - pImageCesium->mipPositions[0].byteSize, + pImageAsset->mipPositions.emplace_back(ImageAssetMipPosition{ + pImageAsset->mipPositions[0].byteSize, originalMipPixels.size()}); - pImageCesium->pixelData.resize( + pImageAsset->pixelData.resize( originalPixels.size() + originalMipPixels.size()); std::memcpy( - pImageCesium->pixelData.data(), + pImageAsset->pixelData.data(), originalPixels.data(), originalPixels.size()); std::memcpy( - pImageCesium->pixelData.data() + originalPixels.size(), + pImageAsset->pixelData.data() + originalPixels.size(), originalMipPixels.data(), originalMipPixels.size()); }); @@ -121,9 +121,9 @@ void CesiumTextureUtilitySpec::Define() { } void CesiumTextureUtilitySpec::RunTests() { - It("ImageCesium non-sRGB", [this]() { + It("ImageAsset non-sRGB", [this]() { TUniquePtr pHalfLoaded = loadTextureAnyThreadPart( - *pImageCesium, + *pImageAsset, TextureAddress::TA_Mirror, TextureAddress::TA_Wrap, TextureFilter::TF_Bilinear, @@ -145,9 +145,9 @@ void CesiumTextureUtilitySpec::RunTests() { CheckGroup(pRefCountedTexture, TextureGroup::TEXTUREGROUP_Cinematic); }); - It("ImageCesium sRGB", [this]() { + It("ImageAsset sRGB", [this]() { TUniquePtr pHalfLoaded = loadTextureAnyThreadPart( - *pImageCesium, + *pImageAsset, TextureAddress::TA_Clamp, TextureAddress::TA_Mirror, TextureFilter::TF_Trilinear, @@ -178,7 +178,7 @@ void CesiumTextureUtilitySpec::RunTests() { TUniquePtr pHalfLoaded = loadTextureFromImageAndSamplerAnyThreadPart( - *pImageCesium, + *pImageAsset, sampler, false); TestNotNull("pHalfLoaded", pHalfLoaded.Get()); @@ -199,7 +199,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.pCesium = pImageCesium; + image.pCesium = pImageAsset; Sampler& sampler = model.samplers.emplace_back(); sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; @@ -232,7 +232,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.pCesium = pImageCesium; + image.pCesium = pImageAsset; Sampler& sampler1 = model.samplers.emplace_back(); sampler1.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; @@ -299,7 +299,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.pCesium = pImageCesium; + image.pCesium = pImageAsset; Sampler& sampler = model.samplers.emplace_back(); sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; @@ -348,7 +348,7 @@ void CesiumTextureUtilitySpec::RunTests() { Model model; Image& image = model.images.emplace_back(); - image.pCesium = pImageCesium; + image.pCesium = pImageAsset; Sampler& sampler = model.samplers.emplace_back(); sampler.minFilter = Sampler::MinFilter::LINEAR_MIPMAP_LINEAR; diff --git a/Source/CesiumRuntime/Public/CesiumPropertyTextureProperty.h b/Source/CesiumRuntime/Public/CesiumPropertyTextureProperty.h index c9c855b5a..7d4b9a056 100644 --- a/Source/CesiumRuntime/Public/CesiumPropertyTextureProperty.h +++ b/Source/CesiumRuntime/Public/CesiumPropertyTextureProperty.h @@ -98,7 +98,7 @@ struct CESIUMRUNTIME_API FCesiumPropertyTextureProperty { const int64 getTexCoordSetIndex() const; const CesiumGltf::Sampler* getSampler() const; - const CesiumGltf::ImageCesium* getImage() const; + const CesiumGltf::ImageAsset* getImage() const; const std::optional getTextureTransform() const; diff --git a/extern/cesium-native b/extern/cesium-native index 526e66068..883e36f10 160000 --- a/extern/cesium-native +++ b/extern/cesium-native @@ -1 +1 @@ -Subproject commit 526e660687d64d98b3f1aea3b8338dafbac0b1db +Subproject commit 883e36f100c6a1487bf1103bafb937a66578705b