Visualize hidden infrastructure in its real-world location using augmented reality.
Use case
You can use AR to "x-ray" the ground to see pipes, wiring, or other infrastructure that isn't otherwise visible. For example, you could use this feature to trace the flow of water through a building to help identify the source of a leak.
How to use the sample
When you open the sample, you'll see a map centered on your current location. Tap on the map to draw pipes around your location. After drawing the pipes, input an elevation offset value to place the drawn infrastructure above or below ground. When you are ready, tap the camera button to view the infrastructure you drew in AR.
How it works
- Draw pipes on the map. See the "Create and edit geometries" sample to learn how to use the geometry editor for creating graphics.
- Add a
WorldScaleSceneView
composable to the augmented reality screen, available in the ArcGIS Maps SDK for Kotlin toolkit.- The component is available both in
World tracking
andGeospatial tracking
modes. Geospatial tracking uses street view data to calibrate augmented reality positioning and is available with an ARCORE API key.
- The component is available both in
- Pass a
SceneView
into the world scale scene view and set the base surface background grid to not be visible and the base surface opacity to 0.0. - Create an
ArcGISTiledElevationSource
and add it to the scene's base surface. Set the navigation constraint to unconstrained to allow going underground if needed. - Configure a graphics overlay and renderer for showing the drawn pipes. This sample uses a
SolidStrokeSymbolLayer
with aMultilayerPolylineSymbol
to draw the pipes.
Relevant API
- GeometryEditor
- GraphicsOverlay
- MultilayerPolylineSymbol
- SolidStrokeSymbolLayer
- Surface
- WorldScaleSceneView
About the data
This sample uses Esri's world elevation service to ensure that the infrastructure you create is accurately placed beneath the ground.
Real-scale AR relies on having data in real-world locations near the user. It isn't practical to provide pre-made data like other ArcGIS Maps SDKs for Native Apps samples, so you must draw your own nearby sample "pipe infrastructure" prior to starting the AR experience.
Additional information
You may notice that pipes you draw underground appear to float more than you would expect. That floating is a normal result of the parallax effect that looks unnatural because you're not used to being able to see underground/obscured objects. Compare the behavior of underground pipes with equivalent pipes drawn above the surface - the behavior is the same, but probably feels more natural above ground because you see similar scenes day-to-day (e.g. utility wires).
This sample requires a device that is compatible with ARCore.
Unlike other scene samples, there's no need for a basemap while navigating, because context is provided by the camera feed showing the real environment. The base surface's opacity is set to zero to prevent it from interfering with the AR experience.
This sample uses the WorldScaleSceneView toolkit component. For information about setting up the toolkit, as well as code for the underlying component, visit the toolkit docs.
Note that apps using ARCore must comply with ARCore's user privacy requirements. See this page for more information.
Tags
augmented reality, full-scale, infrastructure, lines, mixed reality, pipes, real-scale, underground, visualization, visualize, world-scale
Sample Code
/* Copyright 2025 Esri
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.esri.arcgismaps.sample.augmentrealitytoshowhiddeninfrastructure.components
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.Color
import com.arcgismaps.geometry.GeodeticCurveType
import com.arcgismaps.geometry.GeometryEngine
import com.arcgismaps.geometry.LinearUnit
import com.arcgismaps.geometry.Point
import com.arcgismaps.geometry.Polyline
import com.arcgismaps.geometry.PolylineBuilder
import com.arcgismaps.geometry.SpatialReference
import com.arcgismaps.mapping.ArcGISScene
import com.arcgismaps.mapping.ElevationSource
import com.arcgismaps.mapping.NavigationConstraint
import com.arcgismaps.mapping.symbology.MultilayerPolylineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbol
import com.arcgismaps.mapping.symbology.SimpleLineSymbolStyle
import com.arcgismaps.mapping.symbology.SolidStrokeSymbolLayer
import com.arcgismaps.mapping.symbology.StrokeSymbolLayerLineStyle3D
import com.arcgismaps.mapping.view.Graphic
import com.arcgismaps.mapping.view.GraphicsOverlay
import com.arcgismaps.mapping.view.SurfacePlacement
import com.arcgismaps.toolkit.ar.WorldScaleSceneViewProxy
import com.arcgismaps.toolkit.ar.WorldScaleVpsAvailability
import kotlinx.coroutines.launch
class AugmentedRealityViewModel(app: Application) : AndroidViewModel(app) {
val worldScaleSceneViewProxy = WorldScaleSceneViewProxy()
var isVpsAvailable by mutableStateOf(false)
// Graphics overlay for the 3D pipes
val pipeGraphicsOverlay = GraphicsOverlay().apply {
sceneProperties.surfacePlacement = SurfacePlacement.Absolute
}
// Graphics overlay for the shadow of pipes underground
val pipeShadowGraphicsOverlay = GraphicsOverlay().apply {
opacity = 0.6f
}
// Graphics overlay for the leaders
val leaderGraphicsOverlay = GraphicsOverlay().apply {
sceneProperties.surfacePlacement = SurfacePlacement.Absolute
}
// Create a scene with an elevation source and grid and surface hidden
val arcGISScene = ArcGISScene().apply {
baseSurface.apply {
elevationSources.add(ElevationSource.fromTerrain3dService())
backgroundGrid.isVisible = false
opacity = 0.0f
navigationConstraint = NavigationConstraint.None
}
}
// Define a red 3D stroke symbol to show the pipe
private val pipeStrokeSymbol = SolidStrokeSymbolLayer(
width = 0.3,
color = Color.red,
lineStyle3D = StrokeSymbolLayerLineStyle3D.Tube
)
val pipeSymbol = MultilayerPolylineSymbol(listOf(pipeStrokeSymbol))
// Define a red 2D stroke symbol to show the pipe shadow
private val pipeShadowSymbol = SimpleLineSymbol(
style = SimpleLineSymbolStyle.Solid,
color = Color.yellow,
width = 0.3f
)
val leaderSymbol = SimpleLineSymbol(
style = SimpleLineSymbolStyle.Dash,
color = Color.red,
width = 0.1f
)
init {
// For each pipe in the shared repository
SharedRepository.pipeInfoList.forEach {
viewModelScope.launch {
// Densify the polyline to ensure it has enough points for elevation sampling
val densifiedPolyline = GeometryEngine.densifyGeodeticOrNull(
geometry = it.polyline,
maxSegmentLength = 1.0,
lengthUnit = LinearUnit.meters,
curveType = GeodeticCurveType.Geodesic
) as Polyline
// Add Z values to the polyline using the base surface elevation and elevation offset
val densifiedPolylineWithZ = addZValues(densifiedPolyline, it.elevationOffset)
// Add the 3D pipe to the pipe graphics overlay
pipeGraphicsOverlay.graphics.add(Graphic(densifiedPolylineWithZ, pipeSymbol))
// Only add the shadow if the pipe is underground
if (it.elevationOffset < 0) {
// Add the 2D pipe shadow to the shadow graphics overlay
pipeShadowGraphicsOverlay.graphics.add(Graphic(it.polyline, pipeShadowSymbol))
// Get the original polyline with Z values
val originalPolylineWithZ = addZValues(it.polyline, it.elevationOffset)
// Add leader lines connecting pipe vertices to shadow vertices
addLeaderLines(originalPolylineWithZ, it.elevationOffset)
}
}
}
}
/**
* Adds Z values to the polyline by getting the elevation from the base surface.
*/
private suspend fun addZValues(polyline: Polyline, elevationOffset: Float): Polyline {
// Create a new polyline builder to construct the polyline with Z values
val polylineBuilder = PolylineBuilder(SpatialReference(3857))
// For each point in each part of the densified polyline
polyline.parts.forEach { part ->
part.points.forEach { point ->
arcGISScene.baseSurface.elevationSources.first().load().onSuccess {
arcGISScene.baseSurface.getElevation(point).let { elevationResult ->
// Get the elevation at the point
elevationResult.getOrNull()?.let { elevation ->
// Add the point with the elevation offset to the polyline builder
polylineBuilder.addPoint(
GeometryEngine.createWithZ(
point,
elevation + elevationOffset
)
)
}
}
}
}
}
return polylineBuilder.toGeometry()
}
/**
* Adds leader lines from the pipe vertices to the shadow vertices.
*/
private fun addLeaderLines(pipePolyline: Polyline, elevationOffset: Float) {
// For each point in each part of the densified polyline
pipePolyline.parts.forEach { part ->
part.points.forEach { point ->
// Create a line from the 3D pipe vertex to a pont offset by the elevation offset
val offsetPoint = GeometryEngine.createWithZ(
point,
point.z?.minus(elevationOffset)
)
val leaderLine = Polyline(listOf(point, offsetPoint))
leaderGraphicsOverlay.graphics.add(Graphic(leaderLine, leaderSymbol))
}
}
}
/**
* Checks if the current viewpoint camera location is within the VPS availability area.
*/
fun onCurrentViewpointCameraChanged(location: Point) {
viewModelScope.launch {
worldScaleSceneViewProxy.checkVpsAvailability(location.y, location.x).onSuccess {
isVpsAvailable = it == WorldScaleVpsAvailability.Available
}
}
}
}