diff --git a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h index 840a1d9b5..89feec2e0 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTexturePropertyView.h @@ -135,6 +135,28 @@ int64_t assembleEnumValue( const std::span bytes, PropertyComponentType componentType) noexcept; +/** + * @brief Attempts to obtain an `int64_t` array value from the given span of bytes. + * + * @tparam T The element type to read from `bytes`. + * @param bytes A span of bytes to convert into an array value. + * @returns A \ref PropertyArrayCopy containing the elements read. + */ +PropertyArrayCopy +assembleEnumArrayValue(const std::span bytes, PropertyComponentType componentType) noexcept { + const size_t componentSize = getSizeOfComponentType(componentType); + const size_t numEnums = bytes.size() / componentSize; + std::vector result; + result.reserve(numEnums); + + for(size_t i = 0; i < result.size(); i++) { + const int64_t val = assembleEnumValue(bytes.subspan(i * componentSize), componentType); + result.emplace_back(val); + } + + return PropertyArrayCopy(result); +} + /** * @brief Attempts to obtain a vector value from the given span of bytes. * @@ -650,17 +672,17 @@ class PropertyTexturePropertyView * @return The value of the element, or std::nullopt if it matches the "no * data" value */ - std::optional> + std::optional get(double u, double v) const noexcept { if (this->_status == PropertyTexturePropertyViewStatus::EmptyPropertyWithDefault) { - return propertyValueViewToCopy(this->defaultValue()); + return this->defaultValue(); } - PropertyValueViewToCopy value = getRaw(u, v); + PropertyEnumValue value = getRaw(u, v); if (value == this->noData()) { - return propertyValueViewToCopy(this->defaultValue()); + return this->defaultValue(); } else { return value; } @@ -714,6 +736,227 @@ class PropertyTexturePropertyView PropertyComponentType _componentType; }; +/** + * @brief A view of enum data specified by a + * {@link PropertyTextureProperty}. + * + * Provides utilities to sample the property texture property using texture + * coordinates. + * + * @tparam ElementType The type of the elements represented in the property view + */ +template <> +class PropertyTexturePropertyView> + : public PropertyView>, public TextureView { +public: + /** + * @brief Constructs an invalid instance for a non-existent property. + */ + PropertyTexturePropertyView() noexcept + : PropertyView>(), + TextureView(), + _channels(), + _swizzle() {} + + /** + * @brief Constructs an invalid instance for an erroneous property. + * + * @param status The code from {@link PropertyTexturePropertyViewStatus} indicating the error with the property. + */ + PropertyTexturePropertyView(PropertyViewStatusType status) noexcept + : PropertyView>(status), + TextureView(), + _channels(), + _swizzle() { + CESIUM_ASSERT( + this->_status != PropertyTexturePropertyViewStatus::Valid && + "An empty property view should not be constructed with a valid status"); + } + + /** + * @brief Constructs an instance of an empty property that specifies a default + * value. Although this property has no data, it can return the default value + * when \ref get is called. However, \ref getRaw cannot be used. + * + * @param classProperty The {@link ClassProperty} this property conforms to. + */ + PropertyTexturePropertyView(const ClassProperty& classProperty) noexcept + : PropertyView>(classProperty), + TextureView(), + _channels(), + _swizzle() { + if (this->_status != PropertyTexturePropertyViewStatus::Valid) { + // Don't override the status / size if something is wrong with the class + // property's definition. + return; + } + + if (!classProperty.defaultProperty) { + // This constructor should only be called if the class property *has* a + // default value. But in the case that it does not, this property view + // becomes invalid. + this->_status = + PropertyTexturePropertyViewStatus::ErrorNonexistentProperty; + return; + } + + this->_status = PropertyTexturePropertyViewStatus::EmptyPropertyWithDefault; + } + + /** + * @brief Construct a view of the data specified by a {@link PropertyTextureProperty}. + * + * @param property The {@link PropertyTextureProperty} + * @param classProperty The {@link ClassProperty} this property conforms to. + * @param pEnumDefinition The {@link CesiumGltf::Enum} defining the values for this property. + * @param sampler The {@link Sampler} used by the property. + * @param image The {@link ImageAsset} used by the property. + * @param options The options for constructing the view. + */ + PropertyTexturePropertyView( + const PropertyTextureProperty& property, + const ClassProperty& classProperty, + const CesiumGltf::Enum* pEnumDefinition, + const Sampler& sampler, + const ImageAsset& image, + const TextureViewOptions& options = TextureViewOptions()) noexcept + : PropertyView>(classProperty, pEnumDefinition), + TextureView( + sampler, + image, + property.texCoord, + property.getExtension(), + options), + _channels(property.channels), + _swizzle(), + _componentType( + convertStringToPropertyComponentType(pEnumDefinition->valueType)) { + if (this->_status != PropertyTexturePropertyViewStatus::Valid) { + return; + } + + switch (this->getTextureViewStatus()) { + case TextureViewStatus::Valid: + break; + case TextureViewStatus::ErrorInvalidSampler: + this->_status = PropertyTexturePropertyViewStatus::ErrorInvalidSampler; + return; + case TextureViewStatus::ErrorInvalidImage: + this->_status = PropertyTexturePropertyViewStatus::ErrorInvalidImage; + return; + case TextureViewStatus::ErrorEmptyImage: + this->_status = PropertyTexturePropertyViewStatus::ErrorEmptyImage; + return; + case TextureViewStatus::ErrorInvalidBytesPerChannel: + this->_status = + PropertyTexturePropertyViewStatus::ErrorInvalidBytesPerChannel; + return; + case TextureViewStatus::ErrorUninitialized: + case TextureViewStatus::ErrorInvalidTexture: + default: + this->_status = PropertyTexturePropertyViewStatus::ErrorInvalidTexture; + return; + } + + _swizzle.reserve(_channels.size()); + + for (size_t i = 0; i < _channels.size(); ++i) { + switch (_channels[i]) { + case 0: + _swizzle += "r"; + break; + case 1: + _swizzle += "g"; + break; + case 2: + _swizzle += "b"; + break; + case 3: + _swizzle += "a"; + break; + default: + CESIUM_ASSERT( + false && "A valid channels vector must be passed to the view."); + } + } + } + + /** + * @brief Gets the value of the property for the given texture coordinates. + * The sampler's wrapping mode will be used when sampling the texture. + * + * Value transforms are not applied for enum values, so this method is + * equivalent to @ref getRaw, except that if this property has a specified "no + * data" value, this will return the property's default value for any elements + * that equal this "no data" value. If the property did not specify a default + * value, this returns std::nullopt. + * + * @param u The u-component of the texture coordinates. + * @param v The v-component of the texture coordinates. + * + * @return The value of the element, or std::nullopt if it matches the "no + * data" value + */ + std::optional> + get(double u, double v) const noexcept { + if (this->_status == + PropertyTexturePropertyViewStatus::EmptyPropertyWithDefault) { + return propertyValueViewToCopy(this->defaultValue()); + } + + PropertyArrayCopy value = getRaw(u, v); + + if (value == this->noData()) { + return propertyValueViewToCopy(this->defaultValue()); + } else { + return value; + } + } + + /** + * @brief Gets the raw value of the property for the given texture + * coordinates. The sampler's wrapping mode will be used when sampling the + * texture. + * + * If this property has a specified "no data" value, the raw value will still + * be returned, even if it equals the "no data" value. + * + * @param u The u-component of the texture coordinates. + * @param v The v-component of the texture coordinates. + * + * @return The value at the nearest pixel to the texture coordinates. + */ + + PropertyArrayCopy + getRaw(double u, double v) const noexcept { + CESIUM_ASSERT( + this->_status == PropertyTexturePropertyViewStatus::Valid && + "Check the status() first to make sure view is valid"); + + std::vector sample = + this->sampleNearestPixel(u, v, this->_channels); + + return assembleEnumArrayValue(sample, _componentType); + } + + /** + * @brief Gets the channels of this property texture property. + */ + const std::vector& getChannels() const noexcept { + return this->_channels; + } + + /** + * @brief Gets this property's channels as a swizzle string. + */ + const std::string& getSwizzle() const noexcept { return this->_swizzle; } + +private: + std::vector _channels; + std::string _swizzle; + PropertyComponentType _componentType; +}; + #pragma endregion #pragma region Normalized property diff --git a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h index ddf14c0b7..819b5dfea 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTextureView.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTextureView.h @@ -346,6 +346,10 @@ class PropertyTextureView { propertyOptions); } + if constexpr (IsMetadataEnumArray::value) { + return createEnumArrayPropertyView(classProperty, propertyTextureProperty, propertyOptions); + } + if constexpr (IsMetadataArray::value) { return createArrayPropertyView< typename MetadataArrayType::type, @@ -716,6 +720,102 @@ class PropertyTextureView { propertyOptions); } + PropertyTexturePropertyView> + createEnumArrayPropertyView( + const ClassProperty& classProperty, + const PropertyTextureProperty& propertyTextureProperty, + [[maybe_unused]] const TextureViewOptions& propertyOptions) const { + if (!classProperty.array) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorArrayTypeMismatch); + } + + const PropertyType type = convertStringToPropertyType(classProperty.type); + if (TypeToPropertyType::value != type) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorTypeMismatch); + } + + if (!classProperty.enumType) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorInvalidEnumType); + } + + const auto& enumDefinitionIt = + this->_pEnumDefinitions->find(*classProperty.enumType); + if (enumDefinitionIt == this->_pEnumDefinitions->end()) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorInvalidEnumType); + } + + const Enum* pEnumDefinition = &enumDefinitionIt->second; + + const PropertyComponentType componentType = + convertStringToPropertyComponentType(pEnumDefinition->valueType); + const size_t componentSize = getSizeOfComponentType(componentType); + + // We can only grab up to four bytes from the texture. + if (componentSize > 4) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorUnsupportedProperty); + } + + const int64_t count = classProperty.count.value_or(0); + if(count <= 0 || count > 4) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorUnsupportedProperty); + } + + const size_t arraySize = static_cast(count) * componentSize; + + if(arraySize > 4) { + return PropertyTexturePropertyView>( + PropertyTexturePropertyViewStatus::ErrorUnsupportedProperty); + } + + int32_t samplerIndex; + int32_t imageIndex; + + PropertyViewStatusType status = + getTextureSafe(propertyTextureProperty.index, samplerIndex, imageIndex); + + if (status != PropertyTexturePropertyViewStatus::Valid) { + return PropertyTexturePropertyView>(status); + } + + status = checkSampler(samplerIndex); + if (status != PropertyTexturePropertyViewStatus::Valid) { + return PropertyTexturePropertyView>(status); + } + + status = checkImage(imageIndex); + if (status != PropertyTexturePropertyViewStatus::Valid) { + return PropertyTexturePropertyView>(status); + } + + const CesiumUtility::IntrusivePointer& pImage = + _pModel->images[static_cast(imageIndex)].pAsset; + const std::vector& channels = propertyTextureProperty.channels; + + status = checkChannels(channels, *pImage); + if (status != PropertyTexturePropertyViewStatus::Valid) { + return PropertyTexturePropertyView>(status); + } + + if (channels.size() * static_cast(pImage->bytesPerChannel) != + arraySize) { + return PropertyTexturePropertyViewStatus::ErrorChannelsAndTypeMismatch; + } + + return PropertyTexturePropertyView>( + propertyTextureProperty, + classProperty, + pEnumDefinition, + _pModel->samplers[samplerIndex], + *pImage, + propertyOptions); + } + template PropertyTexturePropertyView createVecNPropertyView( const ClassProperty& classProperty, diff --git a/CesiumGltf/include/CesiumGltf/PropertyTypeTraits.h b/CesiumGltf/include/CesiumGltf/PropertyTypeTraits.h index 78b7a9049..ed13bf385 100644 --- a/CesiumGltf/include/CesiumGltf/PropertyTypeTraits.h +++ b/CesiumGltf/include/CesiumGltf/PropertyTypeTraits.h @@ -588,7 +588,7 @@ struct TypeToNormalizedType>> { * @brief Transforms a property value type from a view to an equivalent type * that owns the data it is viewing. For most property types this is an identity * transformation, because most property types are held by value. However, it - * transforms numeric `PropertyArrayView` to `PropertyArrayCopy` because a + * transforms numeric and enum `PropertyArrayView` to `PropertyArrayCopy` because a * `PropertyArrayView` only has a pointer to the value it is viewing. * * See `propertyValueViewToCopy`. @@ -598,7 +598,7 @@ struct TypeToNormalizedType>> { */ template using PropertyValueViewToCopy = std::conditional_t< - IsMetadataNumericArray::value, + IsMetadataNumericArray::value || IsMetadataEnumArray::value, PropertyArrayCopy::type>, T>; @@ -606,7 +606,7 @@ using PropertyValueViewToCopy = std::conditional_t< * @brief Transforms a property value type from a copy that owns the data it is * viewing to a view into that data. For most property types this is an identity * transformation, because most property types are held by value. However, it - * transforms numeric `PropertyArrayCopy` to `PropertyArrayView`. + * transforms numeric and enum `PropertyArrayCopy` to `PropertyArrayView`. * * See `propertyValueCopyToView`. * @@ -615,7 +615,7 @@ using PropertyValueViewToCopy = std::conditional_t< */ template using PropertyValueCopyToView = std::conditional_t< - IsMetadataNumericArray::value, + IsMetadataNumericArray::value || IsMetadataEnumArray::value, PropertyArrayView::type>, T>; @@ -631,7 +631,7 @@ using PropertyValueCopyToView = std::conditional_t< template static std::optional> propertyValueViewToCopy(const std::optional& view) { - if constexpr (IsMetadataNumericArray::value) { + if constexpr (IsMetadataNumericArray::value || IsMetadataEnumArray::value) { if (view) { return std::make_optional>( std::vector(view->begin(), view->end())); @@ -653,7 +653,7 @@ propertyValueViewToCopy(const std::optional& view) { */ template static PropertyValueViewToCopy propertyValueViewToCopy(const T& view) { - if constexpr (IsMetadataNumericArray::value) { + if constexpr (IsMetadataNumericArray::value || IsMetadataEnumArray::value) { return PropertyValueViewToCopy(std::vector(view.begin(), view.end())); } else { return view; @@ -670,7 +670,7 @@ static PropertyValueViewToCopy propertyValueViewToCopy(const T& view) { */ template static PropertyValueCopyToView propertyValueCopyToView(const T& copy) { - if constexpr (IsMetadataNumericArray::value) { + if constexpr (IsMetadataNumericArray::value || IsMetadataEnumArray::value) { return copy.view(); } else { return copy; diff --git a/CesiumGltf/test/TestPropertyTextureView.cpp b/CesiumGltf/test/TestPropertyTextureView.cpp index 49ecf44db..f26ebb019 100644 --- a/CesiumGltf/test/TestPropertyTextureView.cpp +++ b/CesiumGltf/test/TestPropertyTextureView.cpp @@ -3425,7 +3425,7 @@ TEST_CASE("Test enum PropertyTextureProperty") { } } -/*TEST_CASE("Test array PropertyTextureProperty") { +TEST_CASE("Test enum array PropertyTextureProperty") { Model model; std::vector data = { @@ -3436,7 +3436,7 @@ TEST_CASE("Test enum PropertyTextureProperty") { 77, 0, 191, 223, 28, 11}; - std::vector> expected { + std::vector> expected { { 11, 28, 223 }, { 191, 0, 77}, { 43, 1, 200}, @@ -3503,10 +3503,10 @@ TEST_CASE("Test enum PropertyTextureProperty") { REQUIRE(!classProperty->normalized); SUBCASE("Access correct type") { - PropertyTexturePropertyView> uint8ArrayProperty = - view.getPropertyView>("TestClassProperty"); + PropertyTexturePropertyView> enumArrayProperty = + view.getPropertyView>("TestClassProperty"); REQUIRE( - uint8ArrayProperty.status() == + enumArrayProperty.status() == PropertyTexturePropertyViewStatus::Valid); std::vector texCoords{ @@ -3517,17 +3517,17 @@ TEST_CASE("Test enum PropertyTextureProperty") { for (size_t i = 0; i < texCoords.size(); ++i) { glm::dvec2 uv = texCoords[i]; - const std::array& expectedArray = expected[i]; + const std::array& expectedArray = expected[i]; - PropertyArrayCopy value = - uint8ArrayProperty.getRaw(uv[0], uv[1]); + PropertyArrayCopy value = + enumArrayProperty.getRaw(uv[0], uv[1]); REQUIRE(static_cast(value.size()) == expectedArray.size()); for (int64_t j = 0; j < value.size(); j++) { REQUIRE(value[j] == expectedArray[static_cast(j)]); } - auto maybeValue = uint8ArrayProperty.get(uv[0], uv[1]); + auto maybeValue = enumArrayProperty.get(uv[0], uv[1]); REQUIRE(maybeValue); for (int64_t j = 0; j < maybeValue->size(); j++) { REQUIRE((*maybeValue)[j] == value[j]); @@ -3546,15 +3546,15 @@ TEST_CASE("Test enum PropertyTextureProperty") { extension.scale = {0.5, 0.5}; extension.texCoord = 10; - PropertyTexturePropertyView> uint8ArrayProperty = - view.getPropertyView>( + PropertyTexturePropertyView> enumArrayProperty = + view.getPropertyView>( "TestClassProperty", options); REQUIRE( - uint8ArrayProperty.status() == + enumArrayProperty.status() == PropertyTexturePropertyViewStatus::Valid); - verifyTextureTransformConstruction(uint8ArrayProperty, extension); + verifyTextureTransformConstruction(enumArrayProperty, extension); // This transforms to the following UV values: // (0, 0) -> (0.5, -0.5) -> wraps to (0.5, 0.5) @@ -3578,7 +3578,7 @@ TEST_CASE("Test enum PropertyTextureProperty") { const std::array& expectedArray = expectedTransformed[i]; PropertyArrayCopy value = - uint8ArrayProperty.getRaw(uv[0], uv[1]); + enumArrayProperty.getRaw(uv[0], uv[1]); REQUIRE(static_cast(value.size()) == expectedArray.size()); for (int64_t j = 0; j < value.size(); j++) { @@ -3586,7 +3586,7 @@ TEST_CASE("Test enum PropertyTextureProperty") { } std::optional> maybeValue = - uint8ArrayProperty.get(uv[0], uv[1]); + enumArrayProperty.get(uv[0], uv[1]); REQUIRE(maybeValue); for (int64_t j = 0; j < maybeValue->size(); j++) { REQUIRE((*maybeValue)[j] == value[j]); @@ -3600,12 +3600,12 @@ TEST_CASE("Test enum PropertyTextureProperty") { TextureViewOptions options; options.makeImageCopy = true; - PropertyTexturePropertyView> uint8ArrayProperty = - view.getPropertyView>( + PropertyTexturePropertyView> enumArrayProperty = + view.getPropertyView>( "TestClassProperty", options); REQUIRE( - uint8ArrayProperty.status() == + enumArrayProperty.status() == PropertyTexturePropertyViewStatus::Valid); // Clear the original image data. @@ -3623,7 +3623,7 @@ TEST_CASE("Test enum PropertyTextureProperty") { const std::array& expectedArray = expected[i]; PropertyArrayCopy value = - uint8ArrayProperty.getRaw(uv[0], uv[1]); + enumArrayProperty.getRaw(uv[0], uv[1]); REQUIRE(static_cast(value.size()) == expectedArray.size()); for (int64_t j = 0; j < value.size(); j++) { @@ -3631,7 +3631,7 @@ TEST_CASE("Test enum PropertyTextureProperty") { } std::optional> maybeValue = - uint8ArrayProperty.get(uv[0], uv[1]); + enumArrayProperty.get(uv[0], uv[1]); REQUIRE(maybeValue); for (int64_t j = 0; j < maybeValue->size(); j++) { REQUIRE((*maybeValue)[j] == value[j]); @@ -3669,9 +3669,9 @@ TEST_CASE("Test enum PropertyTextureProperty") { } SUBCASE("Access incorrectly as normalized") { - PropertyTexturePropertyView, true> + PropertyTexturePropertyView, true> normalizedInvalid = - view.getPropertyView, true>( + view.getPropertyView, true>( "TestClassProperty"); REQUIRE( normalizedInvalid.status() == @@ -3681,29 +3681,28 @@ TEST_CASE("Test enum PropertyTextureProperty") { SUBCASE("Channel and type mismatch") { model.images[imageIndex].pAsset->channels = 4; propertyTextureProperty.channels = {0, 1, 2, 3}; - PropertyTexturePropertyView> uint8ArrayProperty = - view.getPropertyView>("TestClassProperty"); + PropertyTexturePropertyView> enumArrayProperty = + view.getPropertyView>("TestClassProperty"); REQUIRE( - uint8ArrayProperty.status() == + enumArrayProperty.status() == PropertyTexturePropertyViewStatus::ErrorChannelsAndTypeMismatch); } SUBCASE("Invalid channel values") { propertyTextureProperty.channels = {0, 4, 1}; - PropertyTexturePropertyView> uint8ArrayProperty = - view.getPropertyView>("TestClassProperty"); + PropertyTexturePropertyView> enumArrayProperty = + view.getPropertyView>("TestClassProperty"); REQUIRE( - uint8ArrayProperty.status() == + enumArrayProperty.status() == PropertyTexturePropertyViewStatus::ErrorInvalidChannels); } SUBCASE("Invalid bytes per channel") { model.images[imageIndex].pAsset->bytesPerChannel = 2; - PropertyTexturePropertyView> uint8ArrayProperty = - view.getPropertyView>("TestClassProperty"); + PropertyTexturePropertyView> enumArrayProperty = + view.getPropertyView>("TestClassProperty"); REQUIRE( - uint8ArrayProperty.status() == + enumArrayProperty.status() == PropertyTexturePropertyViewStatus::ErrorInvalidBytesPerChannel); } -} -*/ \ No newline at end of file +} \ No newline at end of file