Work with 3D attributes

Learn how to filter 3D nodes with 3D attributes associated with a 3D object scene layer. 3D attributes are not supported in the Details Panel UI workflow or through Blueprints. The C++ API will be required to use this feature. In this tutorial you'll use the New York building layer and learn:

  • How to identify which attributes you want to use in your data
  • How to process and give this data to a material
  • How to configure the material to use the attribute data to create an interesting visualization of your data

Prerequisites

Before starting this tutorial, you should:

Follow the Add layers C++ API tutorial and have the New York building layer added in your level.

Steps

Select attributes

Before you begin to write code, you need to know the attribute types you plan to use. By default, materials and the shaders that render them only support float and integer types in Unreal Engine. If you want to use an attribute of a different type, such as strings, you will need to convert these attributes into data that can be processed by the shader.

To learn more about Unreal Engine's materials, visit Unreal Engine's documentation.

  1. Navigate to the layer's service URL and examine the field info to identify which attributes the ArcGIS3DModelLayer supports.

    Use dark colors for code blocksCopy
     
    1
    https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/Buildings_NewYork_17/SceneServer
  2. In this tutorial, you will use attributes named "CNSTRCT_YR" or "NAME". Find both names after "fields":[ and see what type of fields are used for those attributes.

    • "CNSTRCT_YR": "esriFieldTypeInteger"
    • "NAME": "esriFieldTypeString"

Prepare header file and C++ file

Once you know the type of attributes that you will work with, include the necessary headers in .h and .cpp file and prepare for working with two different kinds of attribute types and the editor mode.

  1. Open the API code we created in Add layers C++ API tutorial.

  2. Open your .h file and add the following line.

    Use dark colors for code blocksCopy
     
    1
    #include "ArcGISMapsSDK/BlueprintNodes/GameEngine/Layers/ArcGIS3DModelLayer.h"
  3. Create an enum AttributeType

    Use dark colors for code blocksCopy
           
    1
    2
    3
    4
    5
    6
    7
    UENUM()
    enum class AttributeType : uint8
    {
        None = 0,
        ConstructionYear = 1,
        BuildingName = 2
    };
    
  4. Create the method that will apply the material depending on the "CNSTRCT_YR" and "NAME" data. In this tutorial, you will apply different colors to buildings depending on their built year before or after the year 2000 and materials to some specific buildings.

    Use dark colors for code blocksCopy
           
    1
    2
    3
    4
    5
    6
    7
    void Setup3DAttributes(UArcGIS3DModelLayer* Layer);
    void Setup3DAttributesFloatAndIntegerType(UArcGIS3DModelLayer* Layer);
    void Setup3DAttributesOtherType(UArcGIS3DModelLayer* Layer);
    int32 IsBuildingOfInterest(const FAnsiStringView& buildingName);
    void ForEachString(const Esri::GameEngine::Attributes::Attribute& attribute, std::function<void(const FAnsiStringView&, int32)> predicate);
    UPROPERTY(EditAnywhere, Category = "ArcGISMapsSDK|AttributeType")
        TEnumAsByte<AttributeType> AttributeType;
    
  5. And add the following to enable editor to respond to property changes.

    Use dark colors for code blocksCopy
       
    1
    2
    3
    #if WITH_EDITOR
        void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
    #endif
  6. Open your .cpp file and add necessary headers to work with 3D attributes.

    Use dark colors for code blocksCopy
            
    1
    2
    3
    4
    5
    6
    7
    8
    #include "ArcGISMapsSDK/API/GameEngine/Layers/ArcGIS3DModelLayer.h"
    #include "Materials/Material.h"
    #include "ArcGISMapsSDK/API/GameEngine/Attributes/Attribute.h"
    #include "ArcGISMapsSDK/API/GameEngine/Attributes/VisualizationAttribute.h"
    #include "ArcGISMapsSDK/API/GameEngine/Attributes/VisualizationAttributeDescription.h"
    #include "ArcGISMapsSDK/API/GameEngine/Attributes/VisualizationAttributeType.h"
    #include "ArcGISMapsSDK/API/Unreal/ArrayBuilder.h"
    #include "ArcGISMapsSDK/API/Unreal/ImmutableArray.h"
  7. Add the following lines to update ArcGIS Map in the editor Viewport when you select the attribute type.

    Use dark colors for code blocksCopy
              
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #if WITH_EDITOR
        void AMapCreator::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
        {
            Super::PostEditChangeProperty(PropertyChangedEvent);    ​
            if (PropertyChangedEvent.MemberProperty->GetFName() == GET_MEMBER_NAME_CHECKED(AMapCreator, AttributeType))
            {
                CreateArcGISMap();
            }
        }
    #endif
  8. Call Setup3DAttributes in CreateArcGISMap().

    Use dark colors for code blocksCopy
     
    1
    Setup3DAttributes(buildingLayer);

Pass the attribute data to the materials

Depending on which attributes are going to be visualized, the next step is to pass those attributes and their values to the material being used by the layer.

With float, integer and double typed attributes

When setting an attribute that is either a float or an integer you can simply use these lines of code. Doubles can also be passed directly to the shader. However internally they are being converted to a float so expect some loss of precision.

When "CNSTRCT_YR" is the name of the attribute you want to visualize, this code will publish the "CNSTRCT_YR" data for each object in the layer to the material rendering that layer.

  1. In your .cpp file, create the function.

    Use dark colors for code blocksCopy
        
    1
    2
    3
    4
    void AMapCreator::Setup3DAttributesFloatAndIntegerType(UArcGIS3DModelLayer* Layer)
    {
    
    }
    
  2. In this function, use the following code. When "CNSTRCT_YR" is the name of the attribute you want to visualize, this code will publish the "CNSTRCT_YR" data for each object in the layer to the material rendering that layer.

    Use dark colors for code blocksCopy
         
    1
    2
    3
    4
    5
    auto layerAttributes = Esri::Unreal::ImmutableArray<FString>::CreateBuilder();
    layerAttributes.Add("CNSTRCT_YR");
    
    auto layerAPIObject = StaticCastSharedPtr<Esri::GameEngine::Layers::ArcGIS3DModelLayer>(Layer->APIObject);
    layerAPIObject->SetAttributesToVisualize(layerAttributes.MoveToArray());
  3. Call Setup3DAttributesFloatAndIntegerType(Layer) when attribute type is year.

    Use dark colors for code blocksCopy
           
    1
    2
    3
    4
    5
    6
    7
    void AMapCreator::Setup3DAttributes(UArcGIS3DModelLayer* Layer)
    {
        if (AttributeType == AttributeType::ConstructionYear)
        {
            Setup3DAttributesFloatAndIntegerType(Layer);
        }
    }
    

With other typed attributes

The use of the "NAME" attribute poses an issue with how materials work. GPUs generally do not support strings and other non-numeric data types. To use attributes with other types, you need to use the AttributeProcessor callback to convert these types into meaningful integer or floating point values that are compatible with the material.

For example, if you want to highlight the Empire State building, you can pass a float to the material that is equal to 1.0f when the "NAME" equals "Empire State building" and 0.0f when it does not. You have the freedom to do this however you see fit in the AttributeProcessor.

  1. In your .h file, create a pointer for the AttributeProcessor and set it to private.

    Use dark colors for code blocksCopy
     
    1
    TSharedPtr<Esri::GameEngine::Attributes::AttributeProcessor> AttributeProcessor;
  2. Forward declare the AttributeProcessor and Attribute class.

    Use dark colors for code blocksCopy
               
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    namespace Esri
    {
        namespace GameEngine
        {
            namespace Attributes
            {
                class Attribute;
                class AttributeProcessor;
            } // namespace Attributes
        } // namespace GameEngine
    } // namespace Esri
  3. In your .cpp file, add the necessary header for the AttributeProcessor.

    Use dark colors for code blocksCopy
     
    1
    #include "ArcGISMapsSDK/API/GameEngine/Attributes/AttributeProcessor.h"
  4. Create a function to apply new material to filtered scene nodes.

    Use dark colors for code blocksCopy
        
    1
    2
    3
    4
    void AMapCreator::Setup3DAttributesOtherType(UArcGIS3DModelLayer * Layer)
    {
    
    }
    
  5. Call Setup3DAttributesOtherType(Layer) when attribute type is building name.

    Use dark colors for code blocksCopy
        
    1
    2
    3
    4
    else if (AttributeType == AttributeType::BuildingName)
    {
            Setup3DAttributesOtherType(Layer);
    }
    
  6. In Setup3DAttributesOtherType,"NAME" is the name of the attribute you want to visualize. Its attribute type is esriFieldTypeString, so you will need to configure the AttributeProcessor to pass usable and meaningful values to the material.

    Use dark colors for code blocksCopy
      
    1
    2
    auto layerAttributes = Esri::Unreal::ImmutableArray<FString>::CreateBuilder();
    layerAttributes.Add("NAME");
  7. The attribute description is the buffer that is output to the material. Make IsBuildingOfInterest to output either a 0 or a 1 depending on if the buildings NAME is a name of interest.

    Use dark colors for code blocksCopy
                                    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    auto attributeDescriptions = Esri::Unreal::ImmutableArray<Esri::GameEngine::Attributes::VisualizationAttributeDescription>::CreateBuilder();
    attributeDescriptions.Add(Esri::GameEngine::Attributes::VisualizationAttributeDescription(
        "IsBuildingOfInterest", Esri::GameEngine::Attributes::VisualizationAttributeType::Int32));
    
    // The attribute processor does the work on the CPU of converting the attribute into a value that can be used with the material
    // Integers and floats can be processed the same way as other types, although it is not normally necessary
    AttributeProcessor = ::MakeShared<Esri::GameEngine::Attributes::AttributeProcessor>();
    
    AttributeProcessor->SetProcessEvent(
        [this](const Esri::Unreal::ImmutableArray<Esri::GameEngine::Attributes::Attribute>& inputAttributes,
               const Esri::Unreal::ImmutableArray<Esri::GameEngine::Attributes::VisualizationAttribute>& outputVisualizationAttributes) {
            // Buffers will be provided in the same order they appear in the layer metadata.
            // If layerAttributes contained an additional element, it would be at inputAttributes.At(1)
            const auto nameAttribute = inputAttributes.At(0);
    
            // The outputVisualizationAttributes array expects that its data is indexed the same way as the attributeDescriptions above.
            const auto isBuildingOfInterestAttribute = outputVisualizationAttributes.At(0);
    
            const auto isBuildingOfInterestBuffer = isBuildingOfInterestAttribute.GetData();
            const auto isBuildingOfInterestData =
                TArrayView<int32>(reinterpret_cast<int32*>(isBuildingOfInterestBuffer.GetData()), isBuildingOfInterestBuffer.Num() / sizeof(int32));
    
            // Go over each attribute and if its name is one of the four buildings of interest
            // It sets a "isBuildingOfInterest" value to 1, otherwise it is set to 0
            ForEachString(nameAttribute, [this, &isBuildingOfInterestData](const FAnsiStringView& element, int32 index) {
                isBuildingOfInterestData[index] = IsBuildingOfInterest(element);
            });
        });
    
    // Pass the layer attributes, attribute descriptions and the attribute processor to the layer
    auto layerAPIObject = StaticCastSharedPtr<Esri::GameEngine::Layers::ArcGIS3DModelLayer>(Layer->APIObject);
    layerAPIObject->SetAttributesToVisualize(layerAttributes.MoveToArray(), attributeDescriptions.MoveToArray(), *AttributeProcessor);
  8. Create a function for ForEachString to take care of converting the attribute buffer into a readable string value.

    Use dark colors for code blocksCopy
                              
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    void AMapCreator::ForEachString(const Esri::GameEngine::Attributes::Attribute& attribute,
                                                 std::function<void(const FAnsiStringView&, int32)> predicate)
    {
        const auto buffer = attribute.GetData();
        const auto metadata = reinterpret_cast<const int*>(buffer.GetData());
    
        const auto count = metadata[0];
        assert(count);
    
        const char* string = reinterpret_cast<const char*>(buffer.GetData() + (2 + count) * sizeof(int));
    
        for (int32 i = 0; i < count; ++i)
        {
            auto element = FAnsiStringView();
    
            if (metadata[2 + i] > 0)
            {
                // If the length of the string element is 0, it means the element is null
                element = FAnsiStringView(string, metadata[2 + i] - 1);
            }
    
            predicate(element, i);
    
            string += metadata[2 + i];
        }
    }
    
  9. Register specific building "NAME" value for IsBuildingOfInterest.

    Use dark colors for code blocksCopy
                
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int32 AMapCreator::IsBuildingOfInterest(const FAnsiStringView& buildingName)
    {
        if (buildingName.Equals("Empire State Building") || buildingName.Equals("Chrysler Building") || buildingName.Equals("Tower 1 World Trade Ctr") ||
            buildingName.Equals("One Chase Manhattan Plaza"))
        {
            return 1;
        }
        else
        {
            return 0;
        }
    }
    

Set the material for filtered nodes

You will need to tell the layer which material you are using to render with. This material will later be configured to use the attributes you specified to control how the layer is rendered.

  1. If ConstructionYearRenderer.ConstructionYearRenderer is the name of the material, in Setup3DAttributesFloatAndIntegerType, use the following lines of code to set the MaterialReference in the function to apply new material to filtered scene nodes.

    Use dark colors for code blocksCopy
      
    1
    2
    Layer->SetMaterialReference(
        LoadObject<UMaterial>(this, TEXT("Material'/ArcGISMapsSDK/Samples/AttributeMaterials/ConstructionYearRenderer.ConstructionYearRenderer'")));
  2. If BuildingNameRenderer.BuildingNameRenderer is the name of the material, in Setup3DAttributesOtherType, use the following lines of code to set the MaterialReference.

    Use dark colors for code blocksCopy
      
    1
    2
    Layer->SetMaterialReference(
        LoadObject<UMaterial>(this, TEXT("Material'/ArcGISMapsSDK/Samples/AttributeMaterials/BuildingNameRenderer.BuildingNameRenderer'")));

Configure the material to use the attribute data

Now that the material has access to the attribute data from the previous section, you can configure the material to use the attribute data to render the layer accordingly. There are some helper functions provided in the SDK that allow you to easily access the attribute data according to the data's type in the shader. These helper functions, ReadFeatureFloatAttribute, ReadFeatureIntegerAttribute and ReadFeatureUnsignedIntegerAttribute output the value of the attribute. Which one you use depends on the type of data being exposed in the shader.

With float, integer and double typed attributes

You can use the attribute processor to convert custom data types into a suitable format that can be read by the GPU. If you don't use the attribute processor to convert data, you can use ReadFeatureIntegerAttribute, ReadFeatureUnsignedIntegerAttribute or ReadFeatureFloatAttribute depending on the data. For example, the "CNSTRCT_YR" attribute is an esriFieldTypeInteger type and doesn't have a negative value, so you should use the ReadFeatureUnsignedIntegerAttribute helper function to read the data.

Field typeHelper function (without attribute processor)
esriFieldTypeDateNot supported
esriFieldTypeDoubleReadFeatureFloatAttribute
esriFieldTypeGlobalIDNot supported
esriFieldTypeGUIDNot supported
esriFieldTypeIntegerReadFeatureUnsignedIntegerAttribute, ReadFeatureIntegerAttribute
esriFieldTypeOIDReadFeatureUnsignedIntegerAttribute, ReadFeatureIntegerAttribute
esriFieldTypeSingleReadFeatureFloatAttribute
esriFieldTypeSmallIntegerReadFeatureUnsignedIntegerAttribute, ReadFeatureIntegerAttribute
esriFieldTypeStringNot supported

ConstructionYearRenderer is the material that will reference the attribute textures. This material will consume the attribute data and allow you to render different features within the layer according to their individual attribute data.

In the ConstructionYearRenderer you need to first get the CNSTRCT_YR texture from the layer. Using the CNSTRCT_YR value as an input to the shown If statement, you can render buildings built after the year 2000 in light blue and buildings built before it in yellow.

image

Here is the result of the work. You can see a majority of the buildings in New York City are built prior to the year 2000 but you can also see a collection of new ones as well.

image

With other typed attributes (strings, dates, etc...)

You can convert the data to Float32 or Int32 with the attribute processor. Depending on data type, you should use either ReadFeatureFloatAttribute or ReadFeatureIntegerAttribute helper function. You can also use the attribute processor with float, integer, and double type attributes if you want to process the original data.

In this tutorial, you used "NAME" attribute data to make the "IsBuildingOfInterest" condition output either a 0 or a 1 in Float32. You used the ReadFeatureFloatAttribute because the string attribute was converted and exposed to the shader as a float.

image

Here is the finished product. You can also see in the code a tiled texture was used so that the Empire State Building appeared to have windows.

image

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.