ProGuide Building Multipatches - Esri/arcgis-pro-sdk GitHub Wiki
Language: C#
Subject: Geometry
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 10/06/2024
ArcGIS Pro: 3.4
Visual Studio: 2022
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.
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.
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
- Download and install the sample data required for this guide as instructed in ArcGIS Pro SDK Community Samples Releases.
- Create a new ArcGIS Pro Module Add-in, and name the project RubiksCube. If you are not familiar with the ArcGIS Pro SDK, you can follow the steps in the ProGuide Build your first add-in to get started.
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(PatchType.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 PatchType.FirstRing
but the sides face patch is of type PatchType.TriangleStrip
.
// make the bottom face patch
Patch bottomFacePatch = cubeMultipatchBuilderEx.MakePatch(PatchType.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(PatchType.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(PatchType.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(PatchType.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(PatchType.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(() =>
{
var op = new EditOperation();
op.Name = "Create multipatch feature";
op.SelectNewFeatures = false;
// queue feature creation and track the newly created objectID
RowToken rowToken = op.Create(localSceneLayer, cubeMultipatch);
// execute
bool result = op.Execute();
// if successful
if (result)
{
// save the objectID in the module for other commands to use
Module1.CubeMultipatchObjectID = (long)rowToken.ObjectID;
// 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
RowToken rowToken = op.Create(localSceneLayer, cubeMultipatch);
// execute
bool result = op.Execute();
// if successful
if (result)
{
// save the objectID in the module for other commands to use
Module1.CubeMultipatchObjectID = (long)rowToken.ObjectID;
// 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.
Click the Create Cube button and a new cube multipatch feature will be created.
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 = System.Windows.Media.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 = System.Windows.Media.Color.FromRgb(203, 65, 84);
bottomFaceMaterial.EdgeWidth = 20;
// create material for sides face
BasicMaterial sidesFaceMaterial = new BasicMaterial();
sidesFaceMaterial.Color = System.Windows.Media.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 MultipatchBuilderEx 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();
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 = System.Windows.Media.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 = System.Windows.Media.Color.FromRgb(203, 65, 84);
bottomFaceMaterial.EdgeWidth = 20;
// create material for the sides face
BasicMaterial sidesFaceMaterial = new BasicMaterial();
sidesFaceMaterial.Color = System.Windows.Media.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();
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 == GeometryType.GeometryMultiPatch);
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.CubeMultipatchObjectID, 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 == GeometryType.GeometryMultiPatch);
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.CubeMultipatchObjectID, 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.
Click the Create Cube button to create the multipatch feature. Then click the Apply Materials button to have materials applied to the cube.
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
- JPEG textures. See https://pro.arcgis.com/en/pro-app/latest/sdk/api-reference/topic27324.html
- Uncompressed textures. See https://pro.arcgis.com/en/pro-app/latest/sdk/api-reference/topic27456.html
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", TextureCompressionType.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.
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();
return cubeMultipatchWithTextures;
For reference, the entire ApplyTexturesToMultipatch
function is below. You will also need the GetBufferFromImageFile
function. At 3.0 you need to include the following https://www.nuget.org/packages/Microsoft.Windows.Compatibility for the System.Drawing.Image. Prior to 3.0, 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", TextureCompressionType.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();
return cubeMultipatchWithTextures;
}
// fileName of the form "d:\Temp\Image.jpg"
private byte[] GetBufferFromImageFile(string fileName, TextureCompressionType compressionType)
{
System.Drawing.Image image = System.Drawing.Image.FromFile(fileName);
MemoryStream memoryStream = new MemoryStream();
System.Drawing.Imaging.ImageFormat format = compressionType == TextureCompressionType.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.
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.