ProGuide Building Multipatches - kataya/arcgis-pro-sdk GitHub Wiki

Language:      C#
Subject:       Geometry
Contributor:   ArcGIS Pro SDK Team <[email protected]>
Organization:  Esri, http://www.esri.com
Date:          11/24/2020
ArcGIS Pro:    2.7
Visual Studio: 2017, 2019

Beginning with ArcGIS Pro 2.5, the .NET SDK added support for multipatch creation. In this guide we will explore various features of this new API. The code used to illustrate this guide can be found at the RubiksCube Sample.

Overview

A multipatch is a series of 3-dimensional surfaces used to represent a 3D object. Each part in a multipatch is called a patch and has a list of vertices with 3D coordinates to represent it's shape. A patch geometry can be of type Triangles, Triangle strips, Triangle fans, First Ring and Ring. The type of the patch determines how to interpret the list of vertices.

showPreview

In addition to the geometry, a patch also has optional attributes such as

  • Ms
  • IDs
  • Normals (used to define the direction that each face will use to reflect light)
  • a Material (used to control the visual appearance of the patch)
  • Texture Coordinates (used to define the mapping to apply a 2D texture image).

For a more detailed description of the multipatch geometry structure, please refer to the Esri multipatch white paper.

In this guide we will walk through how to create a simple cube multipatch. Then we will apply materials to the faces of the cube. Finally, we will apply a texture to the faces of the cube so that it looks like a Rubik's cube.

Prerequisites

Step 1

Add a new ArcGIS Pro Add-Ins | ArcGIS Pro Button to the add-in project, and name the item CreateCube.

Modify the config.daml file items as follows:

  • Change the button caption to "Create Cube"
  • Change the button tooltip heading to "Create Cube" and the tooltip text to "Create a cube multipatch."

Open the CreateCube.cs file. Create a new function called CreateCubeMultipatch and have it return a Multipatch object.

private Multipatch CreateCubeMultipatch()
{ 
}

We will use the new MultipatchBuilderEx class to create the multipatch cube. The MultipatchBuilderEx class stores a list of patches for the multipatch under construction. In addition, it also controls attribute awareness of the output multipatch (HasM, HasZ, HasId, HasNormals). The methods on this class do not need to be called on the Main CIM Thread.

We need 3 patches to build a cube : a top face patch, a bottom face patch and the sides face patch. The MultipatchBuilderEx class has a helper method MakePatch which ensures the patch inherits the attribute awareness of the parent builder class. We will use this method to build the individual patches for our cube multipatch.

Initialize the side length of the cube to 5 units.

  double side = 5.0;

Create a MultipatchBuilderEx object.

  // create the multipatch builder
  MultipatchBuilderEx cubeMultipatchBuilderEx = new MultipatchBuilderEx();

Use the MakePatch method to create the top face patch. It will be of type esriPatchType.FirstRing.

  // make the top face patch
  Patch topFacePatch = cubeMultipatchBuilderEx.MakePatch(esriPatchType.FirstRing);

To add points to the patch create a new List<Coordinate3D> and assign it to Patch.Coords

  topFacePatch.Coords = new List<Coordinate3D>
  {
    new Coordinate3D(0, 0, side),
    new Coordinate3D(0, side, side),
    new Coordinate3D(side, side, side),
    new Coordinate3D(side, 0, side),
  };

We will create the bottom face patch and the side face patches in a similar manner. Note that the bottom face patch is also of type esriPatchType.FirstRing but the sides face patch is of type esriPatchType.TriangleStrip.

  // make the bottom face patch
  Patch bottomFacePatch = cubeMultipatchBuilderEx.MakePatch(esriPatchType.FirstRing);
  bottomFacePatch.Coords = new List<Coordinate3D>
  {
    new Coordinate3D(0, 0, 0),
    new Coordinate3D(0, side, 0),
    new Coordinate3D(side, side, 0),
    new Coordinate3D(side, 0, 0),
  };

  // make the sides face patch
  Patch sidesFacePatch = cubeMultipatchBuilderEx.MakePatch(esriPatchType.TriangleStrip);
  sidesFacePatch.Coords = new List<Coordinate3D>
  {
    new Coordinate3D(0, 0, 0),
    new Coordinate3D(0, 0, side),
    new Coordinate3D(side, 0, 0),
    new Coordinate3D(side, 0, side),
    new Coordinate3D(side, side, 0),
    new Coordinate3D(side, side, side),
    new Coordinate3D(0, side, 0),
    new Coordinate3D(0, side, side),
    new Coordinate3D(0, 0, 0),
    new Coordinate3D(0, 0, side),
  };

Now add all three patches to the multipatch builder. MultipatchBuilderEx.Patches returns a reference to the internal list of patches which can be modified directly.

  // add to the Patches collection on the builder
  cubeMultipatchBuilderEx.Patches.Add(topFacePatch);
  cubeMultipatchBuilderEx.Patches.Add(bottomFacePatch);
  cubeMultipatchBuilderEx.Patches.Add(sidesFacePatch);

Finally call MultipatchBuilderEx.ToGeometry to create the immutable multipatch geometry. This is the return value of the function.

  // create the geometry
  Multipatch cubeMultipatch = cubeMultipatchBuilderEx.ToGeometry() as Multipatch;
  return cubeMultipatch;

The entire CreateCubeMultipatch function looks like this

    private Multipatch CreateCubeMultipatch()
    { 
      double side = 5.0;

      // create the multipatch builder
      MultipatchBuilderEx cubeMultipatchBuilderEx = new MultipatchBuilderEx();

      // make the top face patch
      Patch topFacePatch = cubeMultipatchBuilderEx.MakePatch(esriPatchType.FirstRing);
      topFacePatch.Coords = new List<Coordinate3D>
      {
        new Coordinate3D(0, 0, side),
        new Coordinate3D(0, side, side),
        new Coordinate3D(side, side, side),
        new Coordinate3D(side, 0, side),
      };

      // make the bottom face patch
      Patch bottomFacePatch = cubeMultipatchBuilderEx.MakePatch(esriPatchType.FirstRing);
      bottomFacePatch.Coords = new List<Coordinate3D>
      {
        new Coordinate3D(0, 0, 0),
        new Coordinate3D(0, side, 0),
        new Coordinate3D(side, side, 0),
        new Coordinate3D(side, 0, 0),
      };

      // make the sides face patch
      Patch sidesFacePatch = cubeMultipatchBuilderEx.MakePatch(esriPatchType.TriangleStrip);
      sidesFacePatch.Coords = new List<Coordinate3D>
      {
        new Coordinate3D(0, 0, 0),
        new Coordinate3D(0, 0, side),
        new Coordinate3D(side, 0, 0),
        new Coordinate3D(side, 0, side),
        new Coordinate3D(side, side, 0),
        new Coordinate3D(side, side, side),
        new Coordinate3D(0, side, 0),
        new Coordinate3D(0, side, side),
        new Coordinate3D(0, 0, 0),
        new Coordinate3D(0, 0, side),
      };

      // add to the Patches collection on the builder
      cubeMultipatchBuilderEx.Patches.Add(topFacePatch);
      cubeMultipatchBuilderEx.Patches.Add(bottomFacePatch);
      cubeMultipatchBuilderEx.Patches.Add(sidesFacePatch);

      // create the geometry
      Multipatch cubeMultipatch = cubeMultipatchBuilderEx.ToGeometry() as Multipatch;
      return cubeMultipatch;
    }

Move back to your button's OnClick method. In this method, we are going to search for a multipatch layer in the map, create our cube geometry and then use the EditOperation class to create a new feature in the multipatch layer. Any errors will be displayed to the user.

First, search for the multipatch layer in the map.

    // get the multipatch layer from the map
    var localSceneLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>()
                        .FirstOrDefault(l => l.Name == "Cube" && 
                         l.ShapeType == esriGeometryType.esriGeometryMultiPatch);
    if (localSceneLayer == null)
      return;

Next, create the cube multipatch using our CreateCubeMultipatch function.

    // create the multipatch geometry
    var cubeMultipatch = CreateCubeMultipatch();

Finally, jump to the Main CIM thread and use the EditOperation class to create a new feature in the multipatch layer using the cube geometry. Keep track of the newly created objectID. Return any error messages to the main UI thread so they can be displayed to the user with a MessageBox.

     // add the multipatch geometry to the layer
    string msg = await QueuedTask.Run(() =>
    {
      long newObjectID = -1;

      var op = new EditOperation();
      op.Name = "Create multipatch feature";
      op.SelectNewFeatures = false;

      // queue feature creation and track the newly created objectID
      op.Create(localSceneLayer, cubeMultipatch, oid => newObjectID = oid);
      // execute
      bool result = op.Execute();
      // if successful
      if (result)
      {
        // save the objectID in the module for other commands to use
        Module1.CubeMultipatchObjectID = newObjectID;

        // zoom to it
        MapView.Active.ZoomTo(localSceneLayer);

        return "";
      }

      // not successful, return any error message from the EditOperation
      return op.ErrorMessage;
    });

    // if there's an error, show it
    if (!string.IsNullOrEmpty(msg)) 
      MessageBox.Show($@"Multipatch creation failed: " +  msg);

The complete OnClick method for your button should be as follows.

  protected override async void OnClick()
  {
    // get the multipatch layer from the map
    var localSceneLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>()
                        .FirstOrDefault(l => l.Name == "Cube" && 
                         l.ShapeType == esriGeometryType.esriGeometryMultiPatch);
    if (localSceneLayer == null)
      return;

    // create the multipatch geometry
    var cubeMultipatch = CreateCubeMultipatch();

    // add the multipatch geometry to the layer
    string msg = await QueuedTask.Run(() =>
    {
      long newObjectID = -1;

      var op = new EditOperation();
      op.Name = "Create multipatch feature";
      op.SelectNewFeatures = false;

      // queue feature creation and track the newly created objectID
      op.Create(localSceneLayer, cubeMultipatch, oid => newObjectID = oid);
      // execute
      bool result = op.Execute();
      // if successful
      if (result)
      {
        // save the objectID in the module for other commands to use
        Module1.CubeMultipatchObjectID = newObjectID;

        // zoom to it
        MapView.Active.ZoomTo(localSceneLayer);

        return "";
      }

      // not successful, return any error message from the EditOperation
      return op.ErrorMessage;
    });

    // if there's an error, show it
    if (!string.IsNullOrEmpty(msg)) 
      MessageBox.Show($@"Multipatch creation failed: " +  msg);
  }

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open the C:\Data\MultipatchBuilderEx\MultipatchBuilderExCubeDemo.aprx project. Validate the UI by activating the Add-In tab.

showPreview

Click the Create Cube button and a new cube multipatch feature will be created.

showPreview

Stop debugging and return to Visual Studio when you are ready to continue. Don't save your edits.

Step 2

In this step, we will see how to create and apply materials to a multipatch which will enhance the visual appearance of the 3D geometry. The BasicMaterial class has a number of properties to support this.

  • Color. Color of the material specified using RGB values.
  • Transparency Percent. Transparency of the material. It is useful for creating materials such as glass.
  • Shininess. Controls how the light is reflected from the surface.
  • Cull back faces. Back-face culling determines whether the material is visible when projected onto the screen.
  • Edge Color. Color of the edges specified using RGB values.
  • Edge Width. The width of the edges.
  • Texture Resource. The texture applied to a patch.

Add a new ArcGIS Pro Add-Ins | ArcGIS Pro Button to the add-in project, and name the item ApplyMaterials.

Modify the config.daml file items as follows:

  • Change the button caption to "Apply Materials"
  • Change the button tooltip heading to "Apply Materials" and the tooltip text to "Apply materials to the cube multipatch."

Open the ApplyMaterials.cs file. Create a new function called ApplyMaterialsToMultipatch. It should take a Multipatch as a parameter and return a Multipatch object.

private Multipatch ApplyMaterialsToMultipatch(Multipatch source)
{ 
}

Let's start by creating a material for the top face patch using the BasicMaterial object. Then assign values to the Color, Shininess, TransparencyPercent and EdgeWidth properties. The transparency property will allow us to "peek" into the interior of the cube through the top face.

  // create material for the top face patch
  BasicMaterial topFaceMaterial = new BasicMaterial();
  topFaceMaterial.Color = Color.FromRgb(203, 65, 84);
  topFaceMaterial.Shininess = 150;
  topFaceMaterial.TransparencyPercent = 50;
  topFaceMaterial.EdgeWidth = 20;

Follow on with creating the materials for the bottom and side faces. These faces are not transparent so only set the Color and EdgeWidth properties on their materials.

  // create material for bottom face patch
  BasicMaterial bottomFaceMaterial = new BasicMaterial();
  bottomFaceMaterial.Color = Color.FromRgb(203, 65, 84);
  bottomFaceMaterial.EdgeWidth = 20;

  // create material for sides face
  BasicMaterial sidesFaceMaterial = new BasicMaterial();
  sidesFaceMaterial.Color = Color.FromRgb(133, 94, 66);
  sidesFaceMaterial.Shininess = 0;
  sidesFaceMaterial.EdgeWidth = 20;

Now that the materials are defined, we will assign them to the patches. Use the MultipatchBuillderEx to create a new builder object using the input multipatch as the source.

  // create a builder using the source multipatch
  var cubeMultipatchBuilderEx = new MultipatchBuilderEx(source);

Access specific patches using the Patches collection and the appropriate index. The patches are indexed the same as any list; that is in the order they were added to the multipatch builder.

  // set material to the top face patch
  var topFacePatch = cubeMultipatchBuilderEx.Patches[0];
  topFacePatch.Material = topFaceMaterial;
 
  // set material to the bottom face patch
  var bottomFacePatch = cubeMultipatchBuilderEx.Patches[1];
  bottomFacePatch.Material = bottomFaceMaterial;

  // set material to the sides face patch
  var sidesFacePatch = cubeMultipatchBuilderEx.Patches[2];
  sidesFacePatch.Material = sidesFaceMaterial;

Finally create the cube (with materials) using the ToGeometry call. This is the return value of the function.

  // create the geometry
  Multipatch cubeMultipatchWithMaterials = cubeMultipatchBuilderEx.ToGeometry() as Multipatch;
  return cubeMultipatchWithMaterials;

The complete ApplyMaterialsToMultipatch function is below.

    private Multipatch ApplyMaterialsToMultipatch(Multipatch source)
    {
      // create material for the top face patch
      BasicMaterial topFaceMaterial = new BasicMaterial();
      topFaceMaterial.Color = Color.FromRgb(203, 65, 84);
      topFaceMaterial.Shininess = 150;
      topFaceMaterial.TransparencyPercent = 50;
      topFaceMaterial.EdgeWidth = 20;

      // create material for the bottom face patch
      BasicMaterial bottomFaceMaterial = new BasicMaterial();
      bottomFaceMaterial.Color = Color.FromRgb(203, 65, 84);
      bottomFaceMaterial.EdgeWidth = 20;

      // create material for the sides face
      BasicMaterial sidesFaceMaterial = new BasicMaterial();
      sidesFaceMaterial.Color = Color.FromRgb(133, 94, 66);
      sidesFaceMaterial.Shininess = 0;
      sidesFaceMaterial.EdgeWidth = 20;


      // create a builder using the source multipatch
      var cubeMultipatchBuilderEx = new MultipatchBuilderEx(source);

      // set material to the top face patch
      var topFacePatch = cubeMultipatchBuilderEx.Patches[0];
      topFacePatch.Material = topFaceMaterial;

      // set material to the bottom face patch
      var bottomFacePatch = cubeMultipatchBuilderEx.Patches[1];
      bottomFacePatch.Material = bottomFaceMaterial;

      // set material to the sides face patch
      var sidesFacePatch = cubeMultipatchBuilderEx.Patches[2];
      sidesFacePatch.Material = sidesFaceMaterial;

      // create the geometry
      Multipatch cubeMultipatchWithMaterials = cubeMultipatchBuilderEx.ToGeometry() as Multipatch;
      return cubeMultipatchWithMaterials;
    }

Return to the OnClick method. As with the CreateCube button, we will retrieve a multipatch layer from the map. Using the saved objectID value and the layer, the Inspector class will allow us to retrieve the multipatch feature created in the first step. Use the ApplyMaterialsToMultipatch method to apply the materials to the feature's geometry, then the EditOperation class to update the existing multipatch feature with the new multipatch geometry. Any errors are displayed to the user via a MessageBox.

First, search for the multipatch layer in the map.

    // get the multipatch layer from the map
    var localSceneLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>()
                        .FirstOrDefault(l => l.Name == "Cube" && 
                         l.ShapeType == esriGeometryType.esriGeometryMultiPatch);
    if (localSceneLayer == null)
      return;

Next, retrieve the previously created multipatch cube using the Inspector class.

    // get the multipatch shape using the Inspector
    var insp = new Inspector();
    await insp.LoadAsync(localSceneLayer, Module1.CubeMultipatchObjectID);
    var multipatchFromScene = insp.Shape as Multipatch;

Apply materials to the multipatch using our ApplyMaterialsToMultipatch function.

    // apply materials to the multipatch
    var multipatchWithMaterials = ApplyMaterialsToMultipatch(multipatchFromScene);

Finally, jump to the Main CIM thread and use the EditOperation object to modify the feature with the updated geomtry. Return any error messages to the main UI thread so they can be displayed to the user with a MessageBox.

    // modify the multipatch geometry
    string msg = await QueuedTask.Run(() =>
    {
      var op = new EditOperation();
      op.Name = "Apply materials to multipatch";

      // queue feature modification
      op.Modify(localSceneLayer, Module1.CubeMultipatchOID, multipatchWithMaterials);
      // execute
      bool result = op.Execute();
      // if successful
      if (result)
      {
        return "";
      }

      // not successful, return any error message from the EditOperation
      return op.ErrorMessage;
    });

    // if there's an error, show it
    if (!string.IsNullOrEmpty(msg))
      MessageBox.Show($@"Multipatch update failed: " + msg);

The complete OnClick method for your button should be as follows.

    protected override async void OnClick()
    {
      // make sure there's an OID from a created feature
      if (Module1.CubeMultipatchObjectID == -1)
        return;

      //get the multipatch layer from the map
      var localSceneLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>()
                          .FirstOrDefault(l => l.Name == "Cube" && 
                          l.ShapeType == esriGeometryType.esriGeometryMultiPatch);
      if (localSceneLayer == null)
        return;

      // get the multipatch shape using the Inspector
      var insp = new Inspector();
      await insp.LoadAsync(localSceneLayer, Module1.CubeMultipatchObjectID);
      var multipatchFromScene = insp.Shape as Multipatch;

      // apply materials to the multipatch
      var multipatchWithMaterials = ApplyMaterialsToMultipatch(multipatchFromScene);


      // modify the multipatch geometry
      string msg = await QueuedTask.Run(() =>
      {
        var op = new EditOperation();
        op.Name = "Apply materials to multipatch";

        // queue feature modification
        op.Modify(localSceneLayer, Module1.CubeMultipatchOID, multipatchWithMaterials);
        // execute
        bool result = op.Execute();
        // if successful
        if (result)
        {
          return "";
        }

        // not successful, return any error message from the EditOperation
        return op.ErrorMessage;
      });

      // if there's an error, show it
      if (!string.IsNullOrEmpty(msg))
        MessageBox.Show($@"Multipatch update failed: " + msg);
    }

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open the C:\Data\MultipatchBuilderEx\MultipatchBuilderExCubeDemo.aprx project. Validate the UI by activating the Add-In tab.

showPreview

Click the Create Cube button to create the multipatch feature. Then click the Apply Materials button to have materials applied to the cube.

showPreview

Stop debugging and return to Visual Studio when you are ready to continue.

Step 3

So far, we have used solid colors and other properties such as transparency for our materials. In order to make our 3D geometries look more realistic we can define textures in our material and map these textures to the surface of the geometry.

Textures are bitmap images stored in some file formats. Two types of textures are currently supported

The BasicMaterial class contains a reference to a texture resource via the TextureResource property. The TextureResource class wraps an instance of the Texture class. Multiple materials can point to the same texture resource, allowing the sharing of textures between materials. This extra level of indirection allows us to replace or update textures for all the materials that point to the same texture resource.

Let's see how this works. In this ProGuide, we will be using the jpg image of a rubiks cube found in the Community Samples Data. If you followed the instructions in downloading this data, the Rubik_cube.jpg file should be found in the "C:\Data\MultipatchBuilderEx\Textures" folder.

Add a new ArcGIS Pro Add-Ins | ArcGIS Pro Button to the add-in project, and name the item ApplyTextures.

Modify the config.daml file items as follows:

  • Change the button caption to "Apply Textures"
  • Change the button tooltip heading to "Apply Textures" and the tooltip text to "Apply textures to the cube multipatch."

Open the ApplyTextures.cs file. Create a new function called ApplyTexturesToMultipatch. It should take a Multipatch as a parameter and return a Multipatch object.

private Multipatch ApplyTexturesToMultipatch(Multipatch source)
{ 
}

Read a JPEG texture image into a buffer and create an instance of JPEGTexture. Use the utility function GetBufferFromImageFile which is supplied further below.

  // read jpeg file into a buffer.  Create a JPEGTexture
  byte[] imageBuffer = GetBufferFromImageFile(@"C:\Data\MultipatchBuilderEx\Textures\Rubik_cube.jpg", esriTextureCompressionType.CompressionJPEG);
  JPEGTexture rubiksCubeTexture = new JPEGTexture(imageBuffer);

Create a TextureResource from the JPEG texture and assign it to the TextureResoure property of a new BasicMaterial.

  // create a material
  BasicMaterial textureMaterial = new BasicMaterial();

  // create a TextureResource from the JPEGTexture and assign 
  textureMaterial.TextureResource = new TextureResource(rubiksCubeTexture);

Create a new MultipatchBuilderEx object using the source as a parameter and assign the textureMaterial to each of the patches.

  // create a builder using the source multipatch
  var cubeMultipatchBuilderExWithTextures = new MultipatchBuilderEx(source);

  // assign texture material to all the patches
  foreach (Patch p in cubeMultipatchBuilderExWithTextures.Patches)
  {
    p.Material = textureMaterial;
  }

Finally assign the texture coordinates. The process of applying a 2D texture image onto a 3D surface is called texture mapping. The coordinate system for the Rubik's cube image is shown below.

showPreview

We want to make the top face of our Rubik's cube white; hence we map the white face part of the image to the top face by specifying texture coordinates for the top face patch. This is achieved by specifying which [s,t] image coordinates map to each vertex of the top face patch.

  // assign texture coordinates to the top face patch
  Patch topFacePatch = cubeMultipatchBuilderExWithTextures.Patches[0];
  topFacePatch.TextureCoords2D = new List<Coordinate2D>
  {
    new Coordinate2D(0.25, 0.33),
    new Coordinate2D(0.25, 0),
    new Coordinate2D(0.5, 0),
    new Coordinate2D(0.5, 0.33),
    new Coordinate2D(0.25, 0.33)
  };

We do a similar mapping for the bottom face making it yellow.

  // assign texture coordinates to the bottom face patch
  Patch bottomFacePatch = cubeMultipatchBuilderExWithTextures.Patches[1];
  bottomFacePatch.TextureCoords2D = new List<Coordinate2D>
  {
    new Coordinate2D(0.25, 1),
    new Coordinate2D(0.25, 0.66),
    new Coordinate2D(0.5, 0.66),
    new Coordinate2D(0.5, 1),
    new Coordinate2D(0.25, 1)
  };

And finally we assign the other colors to the side faces.

  // assign texture coordinates to the sides face patch
  Patch sidesFacePatch = cubeMultipatchBuilderExWithTextures.Patches[2];
  sidesFacePatch.TextureCoords2D = new List<Coordinate2D>
  {
    new Coordinate2D(0, 0.66),
    new Coordinate2D(0, 0.33),
    new Coordinate2D(0.25, 0.66),
    new Coordinate2D(0.25, 0.33),
    new Coordinate2D(0.5, 0.66),
    new Coordinate2D(0.5, 0.33),
    new Coordinate2D(0.75, 0.66),
    new Coordinate2D(0.75, 0.33),
    new Coordinate2D(1.0, 0.66),
    new Coordinate2D(1.0, 0.33)
  };

Finally we create and return the geometry.

  // create the geometry
  Multipatch cubeMultipatchWithTextures = cubeMultipatchBuilderExWithTextures.ToGeometry() as Multipatch;
  return cubeMultipatchWithTextures;

For reference, the entire ApplyTexturesToMultipatch function is below. You will also need the GetBufferFromImageFile function. You may need to add a reference to the System.Drawing.dll in your VS project in order to compile.

  private Multipatch ApplyTexturesToMultipatch(Multipatch source)
  {
    // read jpeg file into a buffer.  Create a JPEGTexture
    byte[] imageBuffer = GetBufferFromImageFile(@"C:\Data\MultipatchBuilderEx\Textures\Rubik_cube.jpg", esriTextureCompressionType.CompressionJPEG);
    JPEGTexture rubiksCubeTexture = new JPEGTexture(imageBuffer);

    // create a material
    BasicMaterial textureMaterial = new BasicMaterial();

    // create a TextureResource from the JPEGTexture and assign 
    textureMaterial.TextureResource = new TextureResource(rubiksCubeTexture);


    // create a builder using the source multipatch
    var cubeMultipatchBuilderExWithTextures = new MultipatchBuilderEx(source);

    // assign texture material to all the patches
    foreach (Patch p in cubeMultipatchBuilderExWithTextures.Patches)
    {
      p.Material = textureMaterial;
    }

    // assign texture coordinate to patches

    // assign texture coordinates to the top face patch
    Patch topFacePatch = cubeMultipatchBuilderExWithTextures.Patches[0];
    topFacePatch.TextureCoords2D = new List<Coordinate2D>
    {
      new Coordinate2D(0.25, 0.33),
      new Coordinate2D(0.25, 0),
      new Coordinate2D(0.5, 0),
      new Coordinate2D(0.5, 0.33),
      new Coordinate2D(0.25, 0.33)
    };

    // assign texture coordinates to the bottom face patch
    Patch bottomFacePatch = cubeMultipatchBuilderExWithTextures.Patches[1];
    bottomFacePatch.TextureCoords2D = new List<Coordinate2D>
    {
      new Coordinate2D(0.25, 1),
      new Coordinate2D(0.25, 0.66),
      new Coordinate2D(0.5, 0.66),
      new Coordinate2D(0.5, 1),
      new Coordinate2D(0.25, 1)
    };

    // assign texture coordinates to the sides face patch
    Patch sidesFacePatch = cubeMultipatchBuilderExWithTextures.Patches[2];
    sidesFacePatch.TextureCoords2D = new List<Coordinate2D>
    {
      new Coordinate2D(0, 0.66),
      new Coordinate2D(0, 0.33),
      new Coordinate2D(0.25, 0.66),
      new Coordinate2D(0.25, 0.33),
      new Coordinate2D(0.5, 0.66),
      new Coordinate2D(0.5, 0.33),
      new Coordinate2D(0.75, 0.66),
      new Coordinate2D(0.75, 0.33),
      new Coordinate2D(1.0, 0.66),
      new Coordinate2D(1.0, 0.33)
    };

    // create the geometry
    Multipatch cubeMultipatchWithTextures = cubeMultipatchBuilderExWithTextures.ToGeometry() as Multipatch;
    return cubeMultipatchWithTextures;
  }

  // fileName of the form  "d:\Temp\Image.jpg"
  private byte[] GetBufferFromImageFile(string fileName, esriTextureCompressionType compressionType)
  {
    System.Drawing.Image image = System.Drawing.Image.FromFile(fileName);
    MemoryStream memoryStream = new MemoryStream();

    System.Drawing.Imaging.ImageFormat format = compressionType == esriTextureCompressionType.CompressionJPEG ? System.Drawing.Imaging.ImageFormat.Jpeg : System.Drawing.Imaging.ImageFormat.Bmp;
    image.Save(memoryStream, format);
    byte[] imageBuffer = memoryStream.ToArray();

    return imageBuffer;
  }

Return to the OnClick method. As with the ApplyMaterials button, we will retrieve a multipatch layer from the map. Using the saved objectID value and the layer, the Inspector class will allow us to retrieve the multipatch feature created in the first step. Use the ApplyTexturesToMultipatch method to apply the textures to the feature's geometry, then the EditOperation class to update the existing multipatch feature with the new multipatch geometry. Any errors are displayed to the user via a MessageBox.

Here is the final OnClick method.

  protected override async void OnClick()
  {
    // make sure there's an OID from a created feature
    if (Module1.CubeMultipatchObjectID == -1)
      return;

    // get the multipatch layer from the map
    var localSceneLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>()
                          .FirstOrDefault(l => l.Name == "Cube" && 
                                  l.ShapeType == esriGeometryType.esriGeometryMultiPatch);
    if (localSceneLayer == null)
      return;

    // get the multipatch shape using the Inspector
    var insp = new Inspector();
    await insp.LoadAsync(localSceneLayer, Module1.CubeMultipatchObjectID);
    var multipatchFromScene = insp.Shape as Multipatch;

    // apply textures to the multipatch
    var multipatchWithTextures = ApplyTexturesToMultipatch(multipatchFromScene);


    // modify the multipatch geometry
    string msg = await QueuedTask.Run(() =>
    {
      var op = new EditOperation();
      op.Name = "Apply textures to multipatch";

      // queue feature modification
      op.Modify(localSceneLayer, Module1.CubeMultipatchObjectID, multipatchWithTextures);
      // execute
      bool result = op.Execute();
      // if successful
      if (result)
      {
        return "";
      }

      // not successful, return any error message from the EditOperation
      return op.ErrorMessage;
    });

    // if there's an error, show it
    if (!string.IsNullOrEmpty(msg))
      MessageBox.Show($@"Multipatch update failed: " + msg);
  }

Build the sample and fix any compile errors. Debug the add-in and start ArcGIS Pro. Open the C:\Data\MultipatchBuilderEx\MultipatchBuilderExCubeDemo.aprx project. Validate the UI by activating the Add-In tab.

showPreview

Click the Create Cube button to create the multipatch feature. Then click the Apply Materials button to have materials applied to the cube. Finally click the Apply Textures button to have the Rubik's cube image be applied.

showPreview

⚠️ **GitHub.com Fallback** ⚠️