ProConcepts Multipatches - kataya/arcgis-pro-sdk GitHub Wiki
This concepts document covers special considerations for working with multipatches. It augments the overall geometry concepts covered in ProConcepts Geometry
ArcGIS.Core.dll
Language: C#
Subject: Multipatches
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 7/1/2020
ArcGIS Pro: 2.7
Visual Studio: 2017, 2019
A multipatch is a series of 3-dimensional surfaces used to represent a 3D object. It can be used to represent simple objects such as spheres and cubes or complex objects such as buildings and trees. It allows for the storing of texture image, color, transparency and lighting normal vector information within the geometry itself, making it the ideal data type for the representation of realistic-looking 3D features. Multipatches are useful in exchanging data with other non-GIS 3D software packages such as Collaborative Design Activity (COLLADA) and SketchUp®.
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 can be of type Triangles, Triangle Strip, Triangle Fan, First Ring or Ring. The type of the patch determines how to interpret the list of vertices. The following image displays examples of the different patch types.
As with all other geometry builders the general process of constructing a multipatch object is as follows.
- Create an instance of the appropriate builder object; in this instance the MultipatchBuilderEx class.
- Build the parts.
- The MultipatchBuilderEx class has a MakePatch method that creates a new patch of the specified patchType. Add the new patch to the Patches collection.
- Populate or modify patch properties. The patch has lists representing the x,y,z coordinates and M-values along with other properties to control it's visual appearance.
- Use the
ToGeometry
method to obtain the geometry.
The MultipatchBuilderEx and Patch classes contain lists representing the patches, the coordinates, the M-values and other information. These lists can be manipulated directly or by using other methods provided. Note that if you modify a list you are not modifying a copy of the list, but modifying the list itself.
Here is a simple example of a multipatch built with patches of type Triangles. The process is as outlined above; the MultipatchBuilderEx
class is created; a number of patches are created using the MakePatch
method; each patch has coordinates assigned via the Coords
property before being added to a list. This patch list is assigned to the MultipatchBuilderEx.Patches
collection. Finally ToGeometry
is called to obtain the multipatch geometry.
// coordinates
var coords_face1 = new List<Coordinate3D>()
{
new Coordinate3D(12.495461061000071,41.902603910000039,62.552700000000186),
new Coordinate3D(12.495461061000071,41.902603910000039,59.504700000004959),
new Coordinate3D(12.495461061000071,41.902576344000067,59.504700000004959),
new Coordinate3D(12.495461061000071,41.902603910000039,62.552700000000186),
new Coordinate3D(12.495461061000071,41.902576344000067,59.504700000004959),
new Coordinate3D(12.495461061000071,41.902576344000067,62.552700000000186),
};
var coords_face2 = new List<Coordinate3D>()
{
new Coordinate3D(12.495461061000071, 41.902576344000067, 62.552700000000186),
new Coordinate3D(12.495461061000071, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902576344000067, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902576344000067, 62.552700000000186),
};
var coords_face3 = new List<Coordinate3D>()
{
new Coordinate3D(12.495488442000067, 41.902576344000067, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902576344000067, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902603910000039, 62.552700000000186),
};
var coords_face4 = new List<Coordinate3D>()
{
new Coordinate3D(12.495488442000067, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902576344000067, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495488442000067, 41.902603910000039, 59.504700000004959),
};
var coords_face5 = new List<Coordinate3D>()
{
new Coordinate3D(12.495488442000067, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902603910000039, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902603910000039, 59.504700000004959),
new Coordinate3D(12.495461061000071, 41.902603910000039, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902603910000039, 62.552700000000186),
};
var coords_face6 = new List<Coordinate3D>()
{
new Coordinate3D(12.495488442000067, 41.902603910000039, 62.552700000000186),
new Coordinate3D(12.495461061000071, 41.902603910000039, 62.552700000000186),
new Coordinate3D(12.495461061000071, 41.902576344000067, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902603910000039, 62.552700000000186),
new Coordinate3D(12.495461061000071, 41.902576344000067, 62.552700000000186),
new Coordinate3D(12.495488442000067, 41.902576344000067, 62.552700000000186),
};
// create the multipatchBuilderEx object
var mpb = new ArcGIS.Core.Geometry.MultipatchBuilderEx();
// create a list of patch objects
var patches = new List<Patch>();
// make each patch using the appropriate coordinates and add to the patch list
var patch = mpb.MakePatch(esriPatchType.Triangles);
patch.Coords = coords_face1;
patches.Add(patch);
patch = mpb.MakePatch(esriPatchType.Triangles);
patch.Coords = coords_face2;
patches.Add(patch);
patch = mpb.MakePatch(esriPatchType.Triangles);
patch.Coords = coords_face3;
patches.Add(patch);
patch = mpb.MakePatch(esriPatchType.Triangles);
patch.Coords = coords_face4;
patches.Add(patch);
patch = mpb.MakePatch(esriPatchType.Triangles);
patch.Coords = coords_face5;
patches.Add(patch);
patch = mpb.MakePatch(esriPatchType.Triangles);
patch.Coords = coords_face6;
patches.Add(patch);
// assign the patches to the multipatchBuilder
mpb.Patches = patches;
// call ToGeometry to get the multipatch
multipatch = mpb.ToGeometry() as Multipatch;
Following the pattern of all other geometry builders, the MultipatchBuilderEx class also has properties for setting the attribute awareness for Ms and IDs for the resulting geometry - HasM, HasID. These properties work in conjunction with the Ms and IDs properties on each patch.
The MultipatchBuilderEx always recognizes Zs so the HasZ property always returns true.
Here is a snippet illustrating how to set M values on a multipatch. As before, The MultipatchBuilderEx
class is created. The HasM
flag is set. A single patch is created using the MakePatch
method and added to the Patches
collection. Coordinates and M values are assigned via the Coords
and Ms
properties on the patch. Finally the SynchronizeAttributeAwareness
method is called before ToGeometry
to retrieve the multipatch.
The SynchronizeAttributeAwareness method is important. It is used to ensure that the attribute awareness of the multipatch builder is applied to all of the patches. It also synchronizes the size of the Ms, IDs list to that of the Coords list for all patches.
// create the builder
MultipatchBuilderEx mpb = new MultipatchBuilderEx();
// set the HasM flag
mpb.HasM = true;
// make a new patch of type Triangles
mpb.Patches.Add(mpb.MakePatch(esriPatchType.Triangles));
// get the patch
Patch pb = mpb.Patches.Last();
// add the coordinates and the M values
pb.Coords.Add(new Coordinate3D(0, 0, 0)); pb.Ms.Add(0);
pb.Coords.Add(new Coordinate3D(1, 0, 0)); pb.Ms.Add(1);
pb.Coords.Add(new Coordinate3D(1, 1, -10)); pb.Ms.Add(20);
pb.Coords.Add(new Coordinate3D(0, 0, 1)); pb.Ms.Add(3);
pb.Coords.Add(new Coordinate3D(1, 0, 1)); pb.Ms.Add(4);
pb.Coords.Add(new Coordinate3D(1, 1, 1)); pb.Ms.Add(5);
// synchronize attribute awareness
pb.SynchronizeAttributeAwareness();
// get the geometry
var multipatch = mpb.ToGeometry();
// multipatch.HasM will be true
This second example illustrates the power of the SynchronizeAttributeAwareness method. When the patch is created it has 5 coordinates, 1 M value and 1 ID value. After setting the HasM and HasID properties on the builder and calling SyncronizeAttributeAwareness the Ms and IDs lists have the same size as the Coords. Default M and ID values have been assigned.
// set up the coordinates
List<Coordinate3D> coords = new List<Coordinate3D>
{
new Coordinate3D(1000, 1000, 0),
new Coordinate3D(1000, 5000, 100),
new Coordinate3D(5000, 5000, 100),
new Coordinate3D(5000, 1000, 0),
new Coordinate3D(1000, 1000, 0)
};
// make the patch and assign the coordinates
Patch patch = new Patch(esriPatchType.FirstRing);
patch.Coords = coords;
// set up the list of Ms and Ids on the patch
// (note we only have 1 value in each list)
List<double> ms = new List<double> { 1.2 };
patch.Ms = ms;
List<int> ids = new List<int> { 1 };
patch.IDs = ids;
// create the builder and set attribute awareness
MultipatchBuilderEx builderEx = new MultipatchBuilderEx();
builderEx.HasM = true;
builderEx.HasID = true;
// add the patch
builderEx.Patches.Add(patch);
// synchronize
bool changed = builderEx.SynchronizeAttributeAwareness();
// the Ms, IDs now have the same count as the coordinates.
// Default values have been assigned to the patch
// M default is double.Nan
// ID default is 0
// builderEx.Patches[0].Coords.Count = 5
// builderEx.Patches[0].Ms.Count = 5
// builderEx.Patches[0].IDs.Count = 5
// builderEx.Patches[0].Ms = (1.2, double.Nan, double.Nan, double.Nan, double.Nan)
// builderEx.Patches[0].Ids = (1, 0, 0, 0, 0)
// get the geometry
var multipatch = mpb.ToGeometry();
In addition to the 3D coordinates, M and ID values, the Patch class has a number of other properties.
- Patch Priority. When overlapping patches are defined within the same multipatch, priorities can be used to specify the order in which patches should be drawn. The larger the number assigned, the higher the patch priority or precedence the patch takes when rendered.
- Normals. The normals of a 3D object are used for lighting. They define the direction that each face will reflect light. They are usually perpendicular to the patch but can also be defined in other directions. Use this property together with the HasNormals property on the MultipatchBuilderEx.
- Material. The material of this patch. It controls the visual appearance.
- Texture Coordinates 2D. The list of 2D coordinates for mapping a 2D texture to the patch.
We will look at materials next.
When defining the patches of your multipatch, you can assign graphic properties to each patch to enhance the visual appearance of the geometry. This is achieved using a Material. 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.
Here is a snippet showing how to create a material with a number of properties set.
BasicMaterial faceMaterial = new BasicMaterial();
faceMaterial.Color = Color.FromRgb(203, 65, 84);
faceMaterial.Shininess = 150;
faceMaterial.TransparencyPercent = 50;
faceMaterial.EdgeWidth = 20;
And assigning this to the patch
patch.Material = faceMaterial;
You can share materials between patches. Here is a snippet illustrating this. The multipatch being built has 2 patches but shares one material.
patch1.Material = faceMaterial;
patch2.Material = faceMaterial;
Multipatch multipatch = builderEx.ToGeometry() as Multipatch;
// multipatch.PartCount = 2 (parts is number of patches)
// multipatch.HasMaterials = true
// multipatch.MaterialCount = 1
If your multipatch has already been created you can retrieve information about the material on a particular patch via methods on the Multipatch object.
bool hasMaterials = multiPatch.HasMaterials;
int materialCount = multiPatch.MaterialCount;
// if the multipatch has materials
if (hasMaterials)
{
// does the patch have a material?
// materialIndex = -1 if the patch does not have a material;
// 0 <= materialIndex < materialCount if the patch does have materials
int materialIndex = multipatch.GetPatchMaterialIndex(patchIndex);
// properties for an individual material (if multipatch.MaterialCount > 0)
var color = multipatch.GetMaterialColor(materialIndex);
var edgeColor = multipatch.GetMaterialEdgeColor(materialIndex);
var edgeWidth = multipatch.GetMaterialEdgeWidth(materialIndex);
var shiness = multipatch.GetMaterialShininess(materialIndex);
var percent = multipatch.GetMaterialTransparencyPercent(materialIndex);
var cullBackFace = multipatch.IsMaterialCullBackface(materialIndex);
In order to make the 3D geometries look more realistic you can define textures in a 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 textures to be replaced or updated for all the materials that point to the same texture resource.
You can use the other BasicMaterial class properties such as Transparency combined with the TextureResource property to configure the material.
esriTextureCompressionType compressionType = esriTextureCompressionType.CompressionJPEG;
byte[] imageBuffer = GetBufferFromImageFile("OrangeSquare.jpg", compressionType);
// create the texture
var texture = new JPEGTexture(imageBuffer);
// create a texture resource
var textureResource = new TextureResource(texture);
// create the material and assign the texture resource
BasicMaterial material = new BasicMaterial();
material.TextureResource = textureResource;
// assign the material to the patch
patch.Material = material;
In addition to specifying the texture resource as part of the material on the patch, the TextureCoords2D property on the patch object is needed to specify the mapping of the image to the 2D face. There should be the same number of vertices in this list as there are in the Coords property. Normally textures will map to a (0,0), (1,1) pattern mapping the entire image to the face, however there may be instances where you want to map a subset of the texture to the face. For example, the following image displays a flat image of a Rubik's cube mapped to a grid.
If only the part of the image containing the nine white squares is to be mapped to a patch, this can be achieved by specifying which [s,t] image coordinates map to each vertex of the patch. Here is a snippet illustrating this.
// 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)
};
As with the materials, you can retrieve information about the textures on a multipatch via methods on the Multipatch object.
bool hasMaterials = multiPatch.HasMaterials;
// if the multipatch has materials
if (hasMaterials)
{
// does the patch have a material?
// materialIndex = -1 if the patch does not have a material;
// 0 <= materialIndex < materialCount if the patch does have materials
int materialIndex = multipatch.GetPatchMaterialIndex(patchIndex);
// texture properties
bool isTextured = multipatch.IsMaterialTextured(materialIndex);
if (isTextured)
{
esriTextureCompressionType compressionType = multipatch.GetMaterialTextureCompressionType(materialIndex);
var texture = multipatch.GetMaterialTexture(materialIndex);
int columnCount = multipatch.GetMaterialTextureColumnCount(materialIndex);
int rowCount = multipatch.GetMaterialTextureRowCount(materialIndex);
int bpp = multipatch.GetMaterialTextureBytesPerPixel(materialIndex);
}
As previously mentioned, multipatches are useful in exchanging data with other non-GIS 3D software packages such as Collaborative Design Activity (COLLADA) and SketchUp®. The MultipatchBuilderEx.From3DModelFile method allows a multipatch to be imported from a supported 3D model file. The 3D model files that are supported are .3DS, .DAE, .FLT, .OBJ, .gITF, .GLB. If the file cannot be read or doesn't contain any geometry an empty multipatch will be returned.
string modelFileName = @"D:\Data\Multipatch\Collada\BrooksidePark\PicnicTable.dae";
try
{
var modelGeom = MultipatchBuilderEx.From3DModelFile(modelFileName);
if (!modelGeom.IsEmpty)
{
// do something with the multipatch geometry
}
}
catch (ArgumentException)
{
// filetype not supported
}
catch ()
{
// other exceptions
}