ProConcepts Voxel Layers - kataya/arcgis-pro-sdk GitHub Wiki

Voxel layers represent multidimensional spatial and temporal information as a 3D volumetric visualization. Their purpose, and usage relevant to the API are discussed.

Language:      C#
Subject:       Voxel Layers
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

In this topic



Background

Voxel layers were added to ArcGIS Pro at version 2.6 and are now added to the public API at version 2.7. Voxel layers can be created from any volumetric data stored in a netCDF formatted data file. To be compliant as a data source for a voxel layer, the netCDF data must be regularily gridded and follow the Climate and Forecast (CF) convention. These regularly gridded cubes, are "voxels". Each voxel cube represents a location in x,y,z where the z dimension can be either elevation/depth in real-world units, or level which is a derived elevation/depth value like isobaric pressure, or time.

The spacing between voxel cubes, also referred to as "voxels", in a voxel layer is identical but can vary between the different axes. In other words, the length of each voxel in the X dimension will always be the same and the width of each voxel in the Y dimension will always be the same but the lengths and widths can be different. The values associated with each voxel are stored as variables, each with a name, description, and data type. Variable data can be continuous (i.e. "quantitative") or discrete (i.e. "qualitative"). Continuous data is floating point and can represent analog values such as temperatures, gas concentrations, or isobaric pressure readings. Discrete data is categorical, and is any value type other than floating point, such as an integer classification scheme for soil or rock types. There can be many variables stored per voxel of both types, continuous and discrete. Refer to What is a voxel layer? for more information.

Requirements

Working with voxel layers and the voxel api requires ArcGIS Pro version 2.7 or later. It also requires use of an advanced license level. When exploring 3D volumetric information, it is important that data is not distorted. Consequently, a voxel layer can only be added to a local scene with the same coordinate system as the voxel layer. If a voxel layer is added to a global scene or 2D map, the voxel layer will be unavailable in the Contents pane and will not draw. Likewise, if a local scene, containing a voxel layer, is converted to a global scene or map, the voxel layer will still show in the table of contents but will not draw.

Determining Voxel layer API requirements:

 //In the Add-in config.daml - desktopVersion should be set to 2.7 or better
 <AddInInfo id="{...}" version="1.0" desktopVersion="2.7">

 //Map must be a local scene and license level must be advanced
 bool canCreateVoxel = false;
 if (MapView.Active.ViewingMode == MapViewingMode.SceneLocal) {
   //license level must be advanced
   canCreateVoxel = (ArcGIS.Core.Licensing.LicenseInformation.Level == 
       ArcGIS.Core.Licensing.LicenseLevels.Advanced);
 }

 if (canCreateVoxel) {
   //use the API
 }

Layer Creation via the API

Layer creation for voxel layers follows the same general pattern(s) used for other ArcGIS Pro layers. Namely, a local Uri to the layer data source, currently limited to netCDF (.nc) files, is specified as the argument to a VoxelLayerCreationParams. The VoxelLayerCreationParams is used as the argument to the LayerFactory templatized CreateLayer overload: T CreateLayer<T>(LayerCreationParams layerDef, ...). Set the type "T" to be VoxelLayer:

 //Must be on the QueuedTask.Run()

 var path = @"C:\MyData\AirQuality_Redlands.nc";
 var createParams = VoxelLayerCreationParams.Create(path);
 
 //Create the layer on a local scene
 LayerFactory.Instance.CreateLayer<VoxelLayer>(createParams, local_scene);

Specifying the Default Variable for Create

Even though voxels within the voxel layer can contain more than one variable profile, only one variable profile can be rendered at any given time. The initial variable used for rendering, or "visualization", is called the default or the default (variable) profile. When creating the voxel layer, the default variable profile can be specified up-front via the VoxelLayerCreationParams.SetDefault(variable) method. The default variable will automatically be set as the selected variable profile when the layer is created.

The VoxelLayerCreationParams provides a IReadOnlyList<VoxelVariableCreationParams> Variables property that returns the list of all variable profiles available within the netCDF data source being used to create the voxel layer (the list of variables corresponds to the same list of variables populated on the Add Multidimensional Voxel Layer dialog box in the Pro UI). Unless otherwise specified, the first variable in the list of variables will be used as the default variable profile when the voxel layer is created.

 //Must be on the QueuedTask.Run()

 //Create a VoxelLayerCreationParams and set the default variable profile
 var path = @"C:\MyData\AirQuality_Redlands.nc";
 var createParams = VoxelLayerCreationParams.Create(path);
 createParams.IsVisible = true;

 //Set the default variable to be the last variable in the list
 createParams.SetDefaultVariable(createParams.Variables.Last());

 //Create the layer
 LayerFactory.Instance.CreateLayer<VoxelLayer>(createParams, local_scene);

Unwanted voxel variables can be removed from the layer by setting the VoxelVariableCreationParams.IsSelected property for the respective variable(s) false. Once the voxel layer is created, the list of variables it contains cannot be changed.

Initial Rendering

When a voxel layer is added to a scene, assuming its visibility is set to true, the data for (the selected variable for) each voxel must be sampled to determine how each voxel will be rendered. Voxel data sets can be very large and cover large surface areas. Therefore the creation of a renderer for a voxel layer is not instantaneous (or even quasi-instantaneous) and will vary depending on the size and complexity of the voxel data. If immediate access to the renderer is required after layer creation, add-ins should subscribe to the MapMemberPropertiesChangedEvent event. The event will fire with a MapMemberEventHint.Renderer event hint to indicate that the voxel renderer for the current default variable has been initialized and can be safely accessed. Immediately attempting to access the voxel renderer on a newly created voxel layer (or a voxel layer that has not previously been rendered in the current Pro session on the given default variable) can return null or a renderer with values that have not been fully provisioned.

 //Must be on the QueuedTask.Run()

 //Subscribe to the MapMemberPropertiesChangedEvent, eg in your dockpane or module initialize
 ArcGIS.Desktop.Mapping.Events.MapMemberPropertiesChangedEvent.Subscribe((args) => {
    //A MapMemberEventHint.Renderer event hint indicates that the voxel layer renderer is ready
    if (args.EventHints.Any(hint => hint == MapMemberEventHint.Renderer)) {
      //TODO update dockpane UI, etc.
    }
 });
			

Voxel Layer Appearance

On the Pro UI, when a voxel layer is selected in the TOC, the "Voxel Layer" tab group is activated which includes an "Appearance" tab. The appearance tab provides a number of different options to configure vertical offset, vertical exaggeration, and various lighting options. These same properties are likewise accessible via the api and are described in this section.

voxel_tab.png

(Refer to Change the appearance of a voxel layer for use of these settings from the Pro UI.)

Elevation

Elevation settings include the cartographic offset and vertical exaggeration. Cartographic offset is an absolute value that raises or lowers the voxel layer relative to the ground surface. A positive value raises the voxel layer, while a negative value lowers the voxel layer. Typically, cartographic offset is set to 0 meters.

Vertical exaggeration is used to scale the height of the voxels. This can be useful if the Z dimension is small or the voxel layer is being compared with other data, such as borehole data, and the exaggeration factors need to match. The default vertical exaggeration value is initially set as a proportion of the x,y extent of the layer and operates in two modes for voxel layers (in the UI, this is configured via the layer properties elevation tab: “From minimum height” and “Z-coordinates”):

  1. "Z-coordinates" mode or ArcGIS.Core.CIM.ExaggerationMode.ScaleZ. This is the same mode as used by other 3D layers. In "Z-coordinates" mode the entire Z is exaggerated. So if the Z for a given voxel is 10 and the vertical exaggeration is set to 500, the exaggerated Z for the voxel would be 5000. The voxel height and Z position are changed.

  2. “From minimum height” mode or ArcGIS.Core.CIM.ExaggerationMode.ScaleVoxelHeight. In this mode only the voxels themselves are exaggerated and not the space between Z = 0 and the minimum height of the voxel. In other words, only the voxel's height is changed and not its Z position (as in the "Z-coordinates" mode which changes both). This mode is useful when viewing the voxel layer together with a basemap, for example.

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //Offset
 var offset = voxelLayer.CartographicOffset;
 //Increase the offset by 100 meters
 voxelLayer.SetCartographicOffset(offset + 100.0);
 
 var exaggeration = voxelLayer.VerticalExaggeration;
 //apply an exaggeration - increase by 100
 voxelLayer.SetVerticalExaggeration(exaggeration + 100.0);

 //Change the mode - this must be done via the CIM
 var def = voxelLayer.GetDefinition() as CIMVoxelLayer;
 def.Layer3DProperties.ExaggerationMode = ExaggerationMode.ScaleZ;

 //note: vertical exaggeration can also be set via the CIM...
 //def.Layer3DProperties.VerticalExaggeration = exaggeration + 100.0;

 //apply the change
 voxelLayer.SetDefinition(def);

Lighting

Lighting settings include Diffuse and Specular lighting options. Each option can be enabled or disabled, same as on the UI, and is set as a ratio from 0.0 to 1.0 (scaled as a percentage on the UI). Diffuse lighting values increase or decrease the scattering of reflected light from the voxel surface. Higher diffuse lighting values replicate the effect of a rough or convoluted surface with increased scattering resulting in a (more) matte effect. Specular lighting values work exactly the opposite and increase or decrease the concentration of light reflected at the same angle from the surface (i.e. scattering is reduced). Higher specular lighting values replicate the effect of a smooth or regular surface and result in a bright, shiny, or metallic effect. Thus, increasing the Diffuse lighting value is similar to increasing the voxel surface roughness whereas increasing the specular lighting values is similar to increasing the voxel surface smoothness. Although they are not mutually exclusive, diffuse and specular lighting values do have an opposite effect.

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //Diffuse Lighting
 if (!voxelLayer.IsDiffuseLightingEnabled)
    voxelLayer.SetDiffuseLightingEnabled(true);
 var diffuse = voxelLayer.DiffuseLighting;
 //set Diffuse lighting to a value between 0 and 1
 voxelLayer.SetDiffuseLighting(0.1); //10%
 
 //Specular Lighting
 if (!voxelLayer.IsSpecularLightingEnabled)
   voxelLayer.SetSpecularLightingEnabled(true);
 var specular = voxelLayer.SpecularLighting;
 //set Diffuse lighting to a value between 0 and 1
 voxelLayer.SetSpecularLighting(0.8); //80%

Variable Profile

Voxels, beyond their dimensions and location, can have both quantitative and/or qualitative data values associated with them, referred to as "variables" or "variable profiles". The active variable is known as the selected variable profile and is shown as the currently selected variable in the variable profile combo box on the appearance tab. When a voxel layer is created, its default variable profile is automatically selected (which can be customized). The selected variable is accessible off the layer via the voxelLayer.SelectedVariableProfile property. Voxel variables are detailed in the Variable Profile section.

Voxel Volume

The voxel volume is determined by the extent of its x, y and z dimensions. The voxel volume represents the extent of the voxel layer in "voxel space". Voxel space ranges from (0, 0, 0) to (max X, max Y, max Z) dimension values respectively - as specified by voxelLayer.GetVolumeSize(). The x and y dimensions always represent coordinate values (in voxel space) however, the Z dimension can be an elevation or time value associated with the voxel.

The voxel volume retrieved via voxelLayer.GetVolumeSize() returns a tuple<double, double, double> representing the max x, y, and z dimension value respectively. Voxels are always spaced at regular intervals along their x and y dimensions (albeit the x and y dimensions may differ). The z dimension can often be irregular. When rendering the voxel layer, the z dimension is "regularized" by dividing the total height of the volume by the number of voxels. The number of voxels is determined from the number of z intervals in the data. As mentioned above, the minimum voxel location is always at (0, 0, 0):

 //Get the first voxel layer in the TOC
 voxelLayer = MapView.Active.Map.GetLayersAsFlattenedList().OfType<VoxelLayer>().FirstOrDefault();
 if (voxelLayer == null)
    return;
 QueuedTask.Run(() => {
    //access the volume
    (double x, double y, double z) = voxelLayer.GetVolumeSize();
    //TODO - use the dimensions
    ...

Note: The returned voxel z dimension max value is not affected by any current vertical exaggeration setting being applied.

Slices

As voxel layers often cover large areas, it can be useful to define an area of interest to view the volume for a specific area rather than the entire dataset. Areas of interest are created on a voxel layer using "slices". Voxels can be sliced vertically to reduce the surface area and horizontally to reduce the voxel layer height. When a slice is applied to a voxel layer, the voxel volume is visually cut, exposing its interior. The volume that is cut will be between the slice and the camera and the volume that is retained will be on the opposite side of the slice (away from the camera). Slices are applied to the voxel volume when its visualization mode is set to VoxelVisualization.Volume.

To create a slice, an add-in creates a SliceDefinition with a name, position within, or on, the voxel layer (given in voxel coordinates*) and a unit vector normal representing both the orientation of the slice and the tilt of the slice face. Orientation runs clockwise from North at 0 degrees through West, South, and East at 90, 180, and 270 degrees respectively. Tilt runs from 0 degrees (vertical) to +/- 90 degrees (horizontal - facing up/down respectively). Developers can use voxelLayer.GetNormal(orientation, tilt) and voxelLayer.GetOrientationAndTilt(unit_vector) to convert back and forth between orientation and tilt and a unit normal vector. Slices are added to the slice container within the voxel layer legend on the TOC.

  //Get the first selected voxel layer in the TOC
  var voxelLayer = MapView.Active.GetSelectedLayers().OfType<VoxelLayer>().FirstOrDefault();
  if (voxelLayer == null)
     return;
  await QueuedTask.Run(()=> {
    //make sure the visualization is set to volume not surface
    if (voxelLayer.Visualization != VoxelVisualization.Volume)
       voxelLayer.SetVisualization(VoxelVisualization.Volume);
    //expand the slice container in the TOC and set its visibility to true.
    voxelLayer.SetSliceContainerExpanded(true);
    voxelLayer.SetSliceContainerVisibility(true);

    //Create a slice that cuts the volume in two
    (double x, double y, double z) = voxelLayer.GetVolumeSize();
    var slice_pos = new Coordinate3D(x / 2, y / 2, z / 2);

    //Create a normal unit vector w/ orientation and tilt of 0.0 degrees
    //The slice will be vertical and oriented along the North/South plane...
    var normal = voxelLayer.GetNormal(0.0, 0.0);
 
    //Create the slice definition and apply it to the voxel layer
    voxelLayer.CreateSlice(new SliceDefinition() {
      Name = "Mid Slice",
      VoxelPosition = slice_pos,
      Normal = normal,
      IsVisible = true
    });
 });

*Recall: Voxel space ranges from (0, 0, 0) to (max X, max Y, max Z) as specified by voxelLayer.GetVolumeSize().

There is no limit to the number of slices that can be created. Slices can be used to visualize areas of interest for all variable profiles.

The collection of slices on a given voxel layer can be retrieved via voxelLayer.GetSlices(). The slice currently selected in the TOC can be retrieved via mapView.GetSelectedSlices().FirstOrDefault(). To change the properties of a slice, such as its orientation and/or tilt, retrieve the slice definition via GetSlices(), apply the relevant updated value(s) to the slice definition, and apply the change(s) by calling voxelLayer.UpdateSlice(slice).

In the following code snippet, the selected slice has its existing orientation changed by +10 degrees and the tilt is changed to 45 degrees:

  var voxelLayer = ...;
  //Must be on the QueuedTask.Run(); Visualization should be Volume

  //Retrieve the slice to be updated
  var slice = voxelLayer.GetSlices().First(s => s.Name == "My slice");
  //or if it is selected in the TOC use: mapView.GetSelectedSlices().FirstOrDefault()

  //Compute an updated normal - convert the normal into orientation and tilt first...
  //change their values...convert _back_ to a normal
  (double orientation, double tilt) = voxelLayer.GetOrientationAndTilt(slice.Normal);
  slice.Normal = voxelLayer.GetNormal(orientation + 10.0, 45.0);

  //Commit the change
  voxelLayer.UpdateSlice(slice);

To delete a slice, retrieve the relevant slice definition and call voxelLayer.DeleteSlice(slice). In this snippet, all the slices currently defined on the layer, are deleted:

 var voxelLayer = ...;
 QueuedTask.Run(()=> {
   
    //Delete all slices
    var slices = voxelLayer.GetSlices();
    foreach (var slice in slices)
      voxelLayer.DeleteSlice(slice);
    
  ...

Voxel Exploration Dockpane

By default, whenever a voxel slice, section, or surface is created - whether via the UI or API - the voxel exploration pane is activated to make the properties of the slice, section, or surface immediately editable. This allows users to fine-tune and adjust the given object's position within the voxel layer, as well as its orientation, and tilt.

voxel_exploration.png

If your add-in has a custom dockpane, that is currently the active dockpane, you may not wish to have the voxel exploration pane automatically activate - especially if its activation results in the exploration pane covering your dockpane on the Pro UI. To prevent the voxel exploration pane automatically activating, set the voxelLayer.AutoShowExploreDockPane property to false. This setting cannot be overridden in the UI. To revert to the default activation behavior, set voxelLayer.AutoShowExploreDockPane back to true.

 var voxelLayer = ...;

 //Prevent the exploration pane from auto-activating
 voxelLayer.AutoShowExploreDockPane = false;

 //TODO - create slice, section, or surface

 //When needed, set auto-activation behavior back
 voxelLayer.AutoShowExploreDockPane = true;

Variable Profiles

Data values associated with voxels are stored as variables, or variable profiles, within the voxel layer. Each variable is represented by a VoxelVariableProfile class instance. VoxelVariableProfiles have a name, description, and data type. The data type can be either VoxelVariableDataType.Continuous or VoxelVariableDataType.Discrete. Variables that are continuous contain floating point values; that are analog ("quantitative") in nature; and are rendered using a stretch renderer of type CIMVoxelStretchRenderer. Variables that are discrete have values other than floating point; the values are qualitative in nature, usually deriving from class categories. Discrete variables are rendered using a unique value renderer of type CIMVoxelUniqueValueRenderer. The collection of available variables within a given voxel layer is accessed by the voxel layer's GetVariableProfiles() method.

Selected Variable Profile

The currently selected variable profile controls the rendering of the voxel layer to include any associated surfaces and/or (dynamic) sections created on the layer (both of which are described later in this document). The currently selected variable profile is accessed off the voxel layer via the SelectedVariableProfile property and set via voxelLayer.SetSelectedVariableProfile(variable). The currently selected variable profile will be shown on the UI on the voxel layer Appearance tab and its name will be listed in the voxel layer legend on the TOC. Only one variable profile can be "selected" or active at any one time.

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //Get the currently selected variable profile
 var variable = voxelLayer.SelectedVariableProfile;

 //Set a variable profile "selected"...
 voxelLayer.SetSelectedVariableProfile(variable);

Variable Renderers

In all other layers with renderers (or colorizers), there is only one renderer (or colorizer) and it is accessed off the layer. In the voxel layer, there can be multiple renderers, one per variable, and each renderer is accessed off the variable whose values it is rendering. If the variable profile is using continuous data, identified by variable.DataType == VoxelVariableDataType.Continuous, the renderer will be a CIMVoxelStretchRenderer. If the variable profile is using discrete data, identified by variable.DataType == VoxelVariableDataType.Discrete, the renderer will be a CIMVoxelUniqueValueRenderer. A renderer is accessed via variable.Renderer. Changes made to the renderer are applied back to the variable profile (not the layer) by calling variable.SetRenderer(renderer).

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

  var variable = voxelLayer.GetVariableProfiles().First();

  //Get the renderer off the _variable_ not the layer...
  var renderer = variable.Renderer;

  //DataType and Renderer type are related...
  if (variable.DataType == VoxelVariableDataType.Continuous) {
     //Renderer will be stretch
     var stretchRenderer = renderer as CIMVoxelStretchRenderer;
     ...
   }
   else if (variable.DataType == VoxelVariableDataType.Discrete) {
     //Renderer will be unique value
     var uvr = renderer as CIMVoxelUniqueValueRenderer;
     ...

Voxel Unique Value Renderer

Similar to unique value renderers on a feature layer, voxel unique value renderers, CIMVoxelUniqueValueRenderer, assign a unique color (but not symbol) to each class, represented as a CIMVoxelColorUniqueValue. There is one CIMVoxelColorUniqueValue defined per discrete data value in the variable data array. Individual unique value classes can be turned on or off to display all unique values or just a subset via the individual CIMVoxelColorUniqueValue.Visible properties. Default colors are assigned to each unique (or "discrete") value based on the CIMVoxelUniqueValueRenderer.ColorRamp that is currently assigned. In the UI, the color ramp is assigned via the Color scheme combo on the symbology dockpane. Note: If a transparency is applied to a given CIMVoxelColorUniqueValue.Color, the transparencies of individual voxels can overlap which can result in a slightly more opaque appearance.

In this snippet, the visibility of the first unique value class is set to false.

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //get the selected variable profile
 var variable = voxelLayer.SelectedVariableProfile;
 //Must be discrete to have a uvr
 if (variable.DataType != VoxelVariableDataType.Discrete)
   return;

 var renderer = variable.Renderer as CIMVoxelUniqueValueRenderer;

 //Arbitrarily, choose the first class
 var unique_value_class = renderer.Classes.ToList().First();

 //Turn its visibility off
 unique_value_class.Visible = false;

 //Apply the change to the renderer
 renderer.Classes = classes.ToArray();
 //apply the changes
 variable.SetRenderer(renderer);

Note: When switching to a voxel variable with a unique value renderer for the first time, unique values from the variable profile are dynamically loaded. So, depending on the size of the voxel layer, all the unique value classes may not be available if the renderer is immediately accessed. A ArcGIS.Desktop.Mapping.Events.MapMemberPropertiesChangedEvent event will fire, with hint type MapMemberEventHint.Renderer, when the renderer is initialized and all unique value classes are available.

Voxel Stretch Renderer

Continuous variable data is rendered using a CIMVoxelStretchRenderer. A stretch renderer "stretches" a range of colors (selected from the current color scheme) between a maximum and minimum color range value. The color range falls within the minimum and maximum data range on the renderer, computed from the entire range of values present in the variable data array. The data range and color range can both be adjusted on the renderer as needed. By default the data range of the renderer covers +/- 4 standard deviations (change this on the UI by unchecking the More->ShowSignificantDataRange checkbox) but that is not a hard and fast rule and can vary depending on the nature of the data in the given voxel variable. Typically, the color range is set as a subset of the current data range on the renderer otherwise colors would end up being assigned to data values that are not present in the variable data array. However, there may be cases where this is desirable if the add-in wants to filter out specific color values for visual analytic purposes.

In this snippet, the color range of a stretch renderer is increased by 10%:

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //get the selected variable profile
 var variable = voxelLayer.SelectedVariableProfile;
 //Must be continuous to have a stretch renderer
 if (variable.DataType != VoxelVariableDataType.Continuous)
   return;

 var renderer = variable.Renderer as CIMVoxelStretchRenderer;

 var color_min = renderer.ColorRangeMin;
 var color_max = renderer.ColorRangeMax;

 //increase color range by 10% of the current difference
 var dif = (color_max - color_min) * 0.05;
 color_min -= dif;
 color_max += dif;

 //make sure we our color range does not exceed the current 
 //renderer data range
 if (color_min < variable.GetVariableStatistics().MinimumValue)
    color_min = variable.GetVariableStatistics().MinimumValue;
 if (color_max > variable.GetVariableStatistics().MaximumValue)
    color_max = variable.GetVariableStatistics().MaximumValue;

 //update the renderer
 renderer.ColorRangeMin = color_min;
 renderer.ColorRangeMax = color_max;

 //apply the changes
 variable.SetRenderer(renderer);

As with voxel unique value renderers, a ArcGIS.Desktop.Mapping.Events.MapMemberPropertiesChangedEvent event will fire, with hint type MapMemberEventHint.Renderer, when all of the voxel variable data has been sampled and the renderer is initialized with the data and color ranges defined.

Note: the corresponding color assigned to a given value by the stretch renderer can always be retrieved using variable.GetIsosurfaceColor(value).

Isosurfaces

Isosurfaces allow for the preservation of a single data value within the voxel layer which can be visualized, and compared against, the currently selected variable profile. An isosurface can be created for any specific value within a continuous variable data type. All voxels represented in an isosurface have the same value (i.e. the surface is homogenous). Up to variable.MaxNumberOfIsosurfaces isosurfaces can be created per variable profile (currently, MaxNumberOfIsosurfaces is set at 4). Isosurfaces cannot be created for discrete variable data types though isosurfaces for discrete data are not necessary. Each discrete value can be visualized independently (just like isosurfaces) by manipulating the visibility of the discrete values within the renderer.

To create an isosurface, add-ins call variable.CreateIsosurface(...) with an IsosurfaceDefinition that has been assigned the specific data value to be visualized. The layer visualization mode must be set to VoxelVisualization.Surface. By default, a color is assigned to the surface based on the current color theme. Add-ins can also assign their own color to the isosurface as needed (e.g. to differentiate the isosurface from (locked) sections defined on the variable). The isosurface color can be color-locked to prevent it from being changed if the color scheme of the stretch renderer (of the parent variable) is changed.

To check whether an isosurface can be created for a given variable profile, call variable.CanCreateIsosurface. CanCreateIsoSurface checks that the variable data type is Continuous; that the current layer visualization mode is Surface; and that the variable.MaxNumberofIsoSurfaces limit has not been reached. Attempting to create a surface for a variable where variable.CanCreateIsosurface returns false will result in an exception.

  //var voxelLayer = ... ;
  //Must be on the QueuedTask.Run()

  //Get the variable profile on which to create the iso surface
  var variable = voxelLayer.SelectedVariableProfile;

  //Visualization mode must be surface
  voxelLayer.SetVisualization(VoxelVisualization.Surface);

   double surface_value = ....;

  //Check surface can be created
  if (variable.CanCreateIsosurface) {
     //Do the create
     variable.CreateIsosurface(new IsosurfaceDefinition() {
        Name = $"Surface for {surface_value}",
        Value = surface_value,
        IsVisible = true
    });
 }

In this example, a custom color is assigned and the isosurface definition is color locked to prevent the color from changing:

  //var voxelLayer = ... ;
  //Must be on the QueuedTask.Run()

  var variable = voxelLayer.SelectedVariableProfile;

 if (variable.CanCreateIsosurface) {
     //Do the create
     variable.CreateIsosurface(new IsosurfaceDefinition() {
        Name = $"Surface for {surface_value}",
        Value = surface_value,
        Color = ColorFactory.Instance.CreateColor(System.Windows.Media.Colors.CornflowerBlue),
        IsCustomColor = true,//lock the color
        IsVisible = true
    });
 }

Isosurfaces are added to the voxel layer isosurface container within the TOC. Check that the isosurface container is visible and expanded (or collapsed) via voxelLayer.IsIsosurfaceContainerVisible and voxelLayer.IsIsosurfaceContainerExpanded respectively. In this example, the visibility and expanded state of the isosurface container is being toggled:

  //var voxelLayer = ... ;
  //Must be on the QueuedTask.Run()

  voxelLayer.SetIsosurfaceContainerExpanded(!voxelLayer.IsIsosurfaceContainerExpanded);
  voxelLayer.SetIsosurfaceContainerVisibility(!voxelLayer.IsIsosurfaceContainerVisible);

The collection of surfaces currently defined on a variable can retrieved via variable.GetIsosurfaces(). To update a surface, change the desired property - name, value, color, and so forth - and apply the surface change via variable.UpdateIsoSurace(...).

An isosurface can be deleted via variable.DeleteIsoSurface(...).

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //var variable = ....;

 var surface = variable.GetIsosurfaces().First();

 //Change the iso surface voxel value
 surface.Value = surface.Value * 0.9;
 
 //update the surface
 variable.UpdateIsosurface(surface);

 //Delete an iso surface
 variable.DeleteIsosurface(variable.GetIsosurfaces().Last());

Sections

A section creates a two-sided vertical or horizontal plane cutting through the voxel layer. Sections are not tied to a variable (unless they are locked), like isosurfaces are, but to their specific location within (or "across") the voxel layer. Sections can reveal valuable information, such as aquifers across a subsurface profile or methane distribution within a riverine water column. Sections can only be visualized and created when the visualization mode is set to VoxelVisualization.Surface.

The creation process for a section is identical to the creation process of a slice except that the add-in creates a SectionDefinition rather than a SliceDefinition and calls voxelLayer.CreateSection(...). Same as for slices, the position of a section is specified in voxel-space coordinates and its orientation and tilt are specified as a unit vector normal. As mentioned, developers can use voxelLayer.GetNormal(orientation, tilt) and voxelLayer.GetOrientationAndTilt(unit_vector) to convert back and forth between orientation and tilt and a unit normal vector.

The symbology of a section will match whatever is the current symbology of the volume (unless the section is locked). Sections are added to the sections container within the voxel layer TOC and each section is uniquely identified with a "system assigned" id, generated when the section is created. Developers can prevent the voxel exploration dockpane from activating when a section is created by setting voxelLayer.AutoShowExploreDockPane to false. Refer to Voxel Exploration Dockpane for more information.

 
 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 //Set visualization to surface and expand the section container and set
 //its visibility true
 if (voxelLayer.Visualization != VoxelVisualization.Surface)
    voxelLayer.SetVisualization(VoxelVisualization.Surface);
 voxelLayer.SetSectionContainerExpanded(true);
 voxelLayer.SetSectionContainerVisibility(true);

 //To stop the Voxel Exploration Dockpane activating use:
 voxelLayer.AutoShowExploreDockPane = false;

 //Create a section that cuts the volume in two on the vertical plane
 var volume = voxelLayer.GetVolumeSize();

 //Orientation 90 degrees (due West), Tilt 0 degrees (vertical)
 var normal = voxelLayer.GetNormal(90, 0.0);
 voxelLayer.CreateSection(new SectionDefinition() {
   Name = "Middle Section",
   VoxelPosition = new Coordinate3D(volume.Item1 / 2, volume.Item2 / 2, volume.Item3 / 2),
   Normal = normal,
   IsVisible = true
 });

In the next example, 3 sections are created. Two are vertical and perpendicular to each other in the x,y plane bisecting the voxel layer. The third is a horizontal plane, bisecting the voxel layer horizontally.

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 var volume = voxelLayer.GetVolumeSize();

 //Make the Normals - each is a Unit Vector (x, y, z)
 var north_south = new Coordinate3D(1, 0, 0);
 var east_west = new Coordinate3D(0, 1, 0);
 var horizontal = new Coordinate3D(0, 0, 1);//or (0,0,-1) - face down

 int n = 0;
 foreach (var normal in new List<Coordinate3D> { north_south, east_west, horizontal }) {
   voxelLayer.CreateSection(new SectionDefinition() {
     Name = $"Cross {++n}",
     VoxelPosition = new Coordinate3D(volume.Item1 / 2, volume.Item2 / 2, volume.Item3 / 2),
     Normal = normal,
     IsVisible = true
   });
 }

The collection of sections for a voxel layer can be retrieved via voxelLayer.GetSections(). The section currently selected in the TOC can be retrieved via mapView.GetSelectedSections().FirstOrDefault().

 var voxelLayer = ...;
 var section = voxelLayer.GetSections().FirstOrDefault();
 var section2 = voxelLayer.GetSections().First(sec => sec.Id == my_section_id);

The voxel layer containing the selected section (in the TOC) can be retrieved via the sectionDefinition.Layer property. This is useful if there is more than one voxel layer in the TOC and the add-in does not have the context of which voxel layer contains the selected section (slice definitions and isosurface definitions have an identical Layer property). To change the properties of a section, including its orientation and/or tilt, update the relevant value(s) and apply the change by calling voxelLayer.UpdateSection(section). In the following code snippet, the selected section has its existing orientation and tilt updated to 45 degrees:

  //Must be on the QueuedTask.Run(); Visualization should be Surface

  //Retrieve the selected section in the TOC
  var section = mapView.GetSelectedSections().First(); 

  //Compute an updated normal - this example uses the "Layer"
  //property to access the section's associated layer
  section.Normal = section.Layer.GetNormal(45.0, 45.0);

  //Apply the change
  section.Layer.UpdateSection(section);

To delete a section, retrieve the section definition and call voxelLayer.DeleteSection(section).

 //Retrieve the selected section in the TOC
 var section = mapView.GetSelectedSections().First(); 
 //Delete it
 QueuedTask.Run(()=> {
    //Delete the selected section
    section.Layer.DeleteSection(section);
  ...

Locked Sections

To create a permanent snapshot of a specific variable, in space and time, a section can be locked. Sections are locked by calling voxelLayer.LockSection(section) which adds a new "locked" section entry to the voxel layer LockedSection container. The LockedSection container is a child container within the voxel layer legend on the TOC. When a section is locked, its corresponding "dynamic" section entry is deleted and removed from the collection of sections. Check if a section can be locked by calling voxelLayer.CanLockSection(section).

 //var voxelLayer = ... ;
 //Must be on the QueuedTask.Run()

 if (voxelLayer.Visualization != VoxelVisualization.Surface)
   voxelLayer.SetVisualization(VoxelVisualization.Surface);
 voxelLayer.SetSectionContainerExpanded(true);
 voxelLayer.SetLockedSectionContainerExpanded(true);
 voxelLayer.SetLockedSectionContainerVisibility(true);

 //lock the currently selected section
 var section = MapView.Active.GetSelectedSections().First();

 //Locking the section creates a locked section, and deletes
 //the section entry.
 if (voxelLayer.CanLockSection(section))
   voxelLayer.LockSection(section);

Once a section is locked, its position and normal cannot be changed. Additionally, A locked section has its symbology “fixed” to the symbology of the selected variable profile when the section was created. The selected variable profile when the section was created is associated with the locked section via its lockedSectionDefinition.VariableName property and is referred to as the locked section’s parent variable. Changing the currently selected variable, after the section has been locked, will not change the locked section's symbology. Only changing the symbology of the originally selected variable, i.e. the parent variable, will change the symbology of the locked section. As the locked section's symbology does not change when different variables are selected, locked section "snapshots" can be visually compared across different variable profiles.

 //Must be on the QueuedTask.Run()

 //Get the currently selected locked section
 var locked_section = MapView.Active.GetSelectedLockedSections().First();

 //Get the variable profile associated with the locked section
 var variable = locked_section.Layer.GetVariableProfile(locked_section.VariableName);

 //set it selected
 locked_section.Layer.SetSelectedVariableProfile(variable);

The collection of locked sections is accessed via voxelLayer.GetLockedSections(). Locked section name, visibility, and expanded/collapsed state in the TOC can be updated. Set these properties on the locked section definition and apply via voxelLayer.UpdateSection(locked_section).

 //var voxelLayer = ...;
 //Must be on the QueuedTask.Run()

 var locked_sections = voxelLayer.GetLockedSections();

 //Toggle all locked sections' visibility
 foreach (var locked_section in locked_sections) {
   locked_section.IsVisible = !locked_section;
   //apply the change
   voxelLayer.UpdateSection(locked_section);
 }

The position and normal of a locked section cannot be changed. To update the position and normal of a locked section, it must be unlocked. When a locked section is unlocked, the "locking" process is reversed. The locked section being unlocked will be removed (from the collection of locked sections) and a (dynamic) section entry for the previously locked section is created (with the same id as before) in the collection of sections for the voxel layer. When, or if, a locked section is unlocked, its symbology changes to the symbology of whichever variable profile is currently selected. Sections are unlocked by calling voxelLayer.UnlockSection(locked_section). Check that a section can be unlocked by calling voxelLayer.CanUnlockSection(locked_section).

 //Must be on the QueuedTask.Run()

 //var locked_section = ...;

 //Unlock the locked section (Deletes the locked section entry, (re)creates
 //a section entry
 if (voxelLayer.CanUnlockSection(locked_section))
   voxelLayer.UnlockSection(locked_section);

To delete a locked section entry directly, add-ins can use voxelLayer.DeleteSection(locked_section).

 //var voxelLayer = ...;
 //Must be on the QueuedTask.Run()

 if (voxelLayer.GetLockedSections().Count() == 0)
   return;

 //Delete the last locked section from the collection of
 //locked sections
 voxelLayer.DeleteSection(voxelLayer.GetLockedSections().Last());
⚠️ **GitHub.com Fallback** ⚠️