Flickering occlusion of Metal CustomLayer by terrain
Environment
- Xcode version: 16.3
- iOS version:
- Devices affected: iPhones, iPad, Macs
- Maps SDK Version: 11.11.0+ and 11.12.beta.1
Observed behavior and steps to reproduce
Occlusion by terrain does product a flickering issue with metal custom layer with depthCompareFunction = .less
Expected behavior
Terrain should occlude the metal layer like any other layer when the metal layer depth stencil descriptor is set to .less.
Notes / preliminary analysis
We have a flickering correct/wrong display of the intersection of our Metal CustomLayer with the terrain like shown on the video below. We tried the same example with only 2 triangles forming a rectangle with the same effect. (depth map and
An interesting note maybe, as it could very well be connected to the same source problem: occlusion by near or far mountains does also flickers between correct (mountains correctly occluding the part of the track being hidden by them) and wrong (track visible in totality).
Additional links and references
https://github.com/user-attachments/assets/353d528a-24a2-4afb-a1c3-da3a814258db
Just adding more screens
https://github.com/user-attachments/assets/f762b4aa-245c-4cd8-b598-01f57952133b
https://github.com/user-attachments/assets/1de7438c-2c0e-44a9-92f3-060f7eea6773
Even the track itself is displayed wrongly 95% of the time like this:
and correctly sometimes:
Not resolved by v11.12.0-rc.1
Hi @rderimay, could you provide some codes on how you setup CustomLayerHost?
Yes sure. This is the one generating the half transparent curtain
final class TrackCurtainCustomLayerHost: NSObject, CustomLayerHost {
private var colorSortedLocations: [(color: UIColor, locations: [TSLocation])] = []
private var terrainExaggeration: Double = 1.0
private var lastZoomScale: Double?
private var metalDevice: MTLDevice?
private var vertexBuffer: MTLBuffer?
private var vertexCount: Int = 0
var depthStencilState: MTLDepthStencilState!
var pipelineState: MTLRenderPipelineState!
func setColorSortedTrack(_ colorSortedLocations: [(color: UIColor, locations: [TSLocation])], terrainExaggeration: Double) {
self.colorSortedLocations = colorSortedLocations
self.terrainExaggeration = terrainExaggeration
vertexBuffer = nil
vertexCount = 0
}
func setTrack(_ locations: [TSLocation], color: UIColor, terrainExaggeration: Double) {
colorSortedLocations = [(color: color, locations: locations)]
self.terrainExaggeration = terrainExaggeration
vertexBuffer = nil
vertexCount = 0
}
func regenerateBufferIfNeeded(zoomScale: Double) {
guard vertexBuffer == nil || (1000 * zoomScale).rounded() != (1000 * (lastZoomScale ?? 0)).rounded() else {
return
}
guard let metalDevice, !colorSortedLocations.isEmpty else {
vertexBuffer = nil
vertexCount = 0
return
}
let data = colorSortedLocations.flatMap { _, locations in
locations.map { location in
let mercator = Projection.project(CLLocationCoordinate2D(location), zoomScale: zoomScale)
return (
vertexData: VertexData(
position: simd_float3(Float(mercator.x), Float(mercator.y), Float(terrainExaggeration * location.altitude.doubleValue)),
color: SIMD4<Float>(0.0, 0.0, 0.0, Float(location.floorHeight ?? 0))
),
floorHeight: Float(terrainExaggeration * Double(location.floorHeight ?? 0))
)
}
}
let vertices = generateVerticalVertices(from: data, thickness: 1.0)
vertexCount = vertices.count
guard vertexCount > 0 else {
vertexBuffer = nil
return
}
vertexBuffer = metalDevice.makeBuffer(bytes: vertices, length: MemoryLayout<VertexData>.stride * vertexCount, options: [])
lastZoomScale = zoomScale
}
func renderingWillStart(_ metalDevice: MTLDevice, colorPixelFormat: UInt, depthStencilPixelFormat: UInt) {
guard let library = metalDevice.makeDefaultLibrary() else {
fatalError("Failed to create shader")
}
guard let vertexFunction = library.makeFunction(name: "vertexShader") else {
fatalError("Could not find vertex function")
}
guard let fragmentFunction = library.makeFunction(name: "fragmentShader") else {
fatalError("Could not find fragment function")
}
self.metalDevice = metalDevice
// --- Setup Vertex Descriptor ---
let vertexDescriptor = MTLVertexDescriptor()
// Position Attribute
vertexDescriptor.attributes[0].format = .float3 // simd_float3
vertexDescriptor.attributes[0].offset = 0
vertexDescriptor.attributes[0].bufferIndex = Int(VertexInputIndexVertices.rawValue)
// Color Attribute
vertexDescriptor.attributes[1].format = .float4 // simd_float4
vertexDescriptor.attributes[1].offset = MemoryLayout<simd_float3>.stride
vertexDescriptor.attributes[1].bufferIndex = Int(VertexInputIndexVertices.rawValue)
// Layout for the buffer containing vertices
vertexDescriptor.layouts[Int(VertexInputIndexVertices.rawValue)].stride = MemoryLayout<VertexData>.stride
vertexDescriptor.layouts[Int(VertexInputIndexVertices.rawValue)].stepRate = 1
vertexDescriptor.layouts[Int(VertexInputIndexVertices.rawValue)].stepFunction = .perVertex
// --- End Vertex Descriptor Setup ---
// Set up pipeline descriptor
let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
pipelineStateDescriptor.label = "Test Layer"
pipelineStateDescriptor.vertexFunction = vertexFunction
pipelineStateDescriptor.vertexDescriptor = vertexDescriptor
pipelineStateDescriptor.fragmentFunction = fragmentFunction
// Set up color attachment (Blending setup seems reasonable for overlays)
let colorAttachment = pipelineStateDescriptor.colorAttachments[0]
colorAttachment?.pixelFormat = MTLPixelFormat(rawValue: colorPixelFormat)!
colorAttachment?.isBlendingEnabled = true
colorAttachment?.rgbBlendOperation = .add
colorAttachment?.sourceRGBBlendFactor = .sourceAlpha // Use source alpha
colorAttachment?.destinationRGBBlendFactor = .oneMinusSourceAlpha // Standard alpha blending
colorAttachment?.alphaBlendOperation = .add
colorAttachment?.sourceAlphaBlendFactor = .one
colorAttachment?.destinationAlphaBlendFactor = .oneMinusSourceAlpha
// Configure render pipeline descriptor depth/stencil
pipelineStateDescriptor.depthAttachmentPixelFormat = MTLPixelFormat(rawValue: depthStencilPixelFormat)!
pipelineStateDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat(rawValue: depthStencilPixelFormat)!
// Configure the depth stencil state
let depthStencilDescriptor = MTLDepthStencilDescriptor()
depthStencilDescriptor.isDepthWriteEnabled = true // Write depth
depthStencilDescriptor.depthCompareFunction = .less // Draw pixels with smaller depth values (closer)
depthStencilState = metalDevice.makeDepthStencilState(descriptor: depthStencilDescriptor)
do {
pipelineState = try metalDevice.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
} catch {
fatalError("Could not make render pipeline state: \(error.localizedDescription)")
}
}
func render(_ parameters: CustomLayerRenderParameters, mtlCommandBuffer: MTLCommandBuffer, mtlRenderPassDescriptor: MTLRenderPassDescriptor) {
let zoomScale = pow(2, parameters.zoom)
regenerateBufferIfNeeded(zoomScale: zoomScale)
guard let vertexBuffer else {
return
}
guard let renderCommandEncoder = mtlCommandBuffer.makeRenderCommandEncoder(descriptor: mtlRenderPassDescriptor) else {
fatalError("Could not create render command encoder from render pass descriptor.")
}
let projectionMatrix = parameters.projectionMatrix.map(\.floatValue)
let viewport = MTLViewport(
originX: 0,
originY: 0,
width: parameters.width,
height: parameters.height,
znear: 0,
zfar: 1
)
renderCommandEncoder.label = "Custom Layer"
renderCommandEncoder.pushDebugGroup("Custom Layer")
renderCommandEncoder.setDepthStencilState(depthStencilState)
renderCommandEncoder.setRenderPipelineState(pipelineState)
renderCommandEncoder.setCullMode(.back)
renderCommandEncoder.setVertexBuffer(vertexBuffer, offset: 0, index: Int(VertexInputIndexVertices.rawValue))
renderCommandEncoder.setVertexBytes(
projectionMatrix,
length: MemoryLayout<simd_float4x4>.size,
index: Int(VertexInputIndexTransformation.rawValue)
)
renderCommandEncoder.drawPrimitives(type: .triangle,
vertexStart: 0,
vertexCount: vertexCount)
renderCommandEncoder.setViewport(viewport)
renderCommandEncoder.popDebugGroup()
renderCommandEncoder.endEncoding()
}
func renderingWillEnd() {
// Unimplemented
}
private func perpendicular2D(_ v: SIMD2<Float>) -> SIMD2<Float> {
SIMD2<Float>(-v.y, v.x)
}
func generateVerticalVertices(from data: [(vertexData: VertexData, floorHeight: Float)], thickness: Float = 1.0) -> [VertexData] {
var vertices: [VertexData] = []
guard data.count > 1 else { return vertices }
let halfWidth = thickness / 2.0
let color = SIMD4<Float>(0.0, 0.0, 0.0, 0.2)
// Crée les faces latérales (devant et derrière) pour chaque segment
for i in 0 ..< data.count - 1 {
let A = data[i].vertexData
let Afloor = data[i].floorHeight
let B = data[i + 1].vertexData
let Bfloor = data[i + 1].floorHeight
// vecteur de segment en XY
let dXY = simd_float2(B.position.x - A.position.x, B.position.y - A.position.y)
// perpendiculaire normalisé en XY
let perp = simd_normalize(simd_float2(-dXY.y, dXY.x))
let offset = perp * halfWidth
// sommets hauts décalés
let A_plus = VertexData(position: A.position + simd_float3(offset.x, offset.y, 0), color: color)
let A_minus = VertexData(position: A.position - simd_float3(offset.x, offset.y, 0), color: color)
let B_plus = VertexData(position: B.position + simd_float3(offset.x, offset.y, 0), color: color)
let B_minus = VertexData(position: B.position - simd_float3(offset.x, offset.y, 0), color: color)
// altitude du sol sous A et B
let zA0 = min(Afloor, A.position.z) // groundElevationProvider(i, simd_float2(A.position.x, A.position.y))
let zB0 = min(Bfloor, B.position.z) // groundElevationProvider(i, simd_float2(B.position.x, B.position.y))
// sommets bas décalés
let A0_plus = VertexData(position: simd_float3(A.position.x + offset.x, A.position.y + offset.y, zA0), color: color)
let A0_minus = VertexData(position: simd_float3(A.position.x - offset.x, A.position.y - offset.y, zA0), color: color)
let B0_plus = VertexData(position: simd_float3(B.position.x + offset.x, B.position.y + offset.y, zB0), color: color)
let B0_minus = VertexData(position: simd_float3(B.position.x - offset.x, B.position.y - offset.y, zB0), color: color)
// Face « + » (avant)
vertices += [
A_plus, B_plus, B0_plus,
A_plus, B0_plus, A0_plus,
]
// Face « – » (arrière)
vertices += [
A_minus, A0_minus, B0_minus,
A_minus, B0_minus, B_minus,
]
}
// --- boucher les extrémités (caps) ---
func addCap(at P: (vertexData: VertexData, floorHeight: Float), next Q: (vertexData: VertexData, floorHeight: Float)) {
let dXY = simd_float2(Q.vertexData.position.x - P.vertexData.position.x, Q.vertexData.position.y - P.vertexData.position.y)
let perp = simd_normalize(simd_float2(-dXY.y, dXY.x))
let offset = perp * halfWidth
let P_plus = P.vertexData.position + simd_float3(offset.x, offset.y, 0)
let P_minus = P.vertexData.position - simd_float3(offset.x, offset.y, 0)
let zP0 = min(P.floorHeight, P.vertexData.position.z) // groundElevationProvider(simd_float2(P.position.vertexData.x, P.position.vertexData.y))
let P0_plus = simd_float3(P_plus.x, P_plus.y, zP0)
let P0_minus = simd_float3(P_minus.x, P_minus.y, zP0)
let vP_plus = VertexData(position: P_plus, color: color)
let vP_minus = VertexData(position: P_minus, color: color)
let vP0_plus = VertexData(position: P0_plus, color: color)
let vP0_minus = VertexData(position: P0_minus, color: color)
// deux triangles pour fermer l’extrémité
vertices += [
vP_minus, vP_plus, vP0_plus,
vP_minus, vP0_plus, vP0_minus,
]
}
// cap au début
// addCap(at: topPoints.first!, next: topPoints[1])
// cap à la fin
// addCap(at: topPoints.last!, next: topPoints[topPoints.count - 2])
return vertices
}
}