mapbox-maps-ios icon indicating copy to clipboard operation
mapbox-maps-ios copied to clipboard

Flickering occlusion of Metal CustomLayer by terrain

Open rderimay opened this issue 9 months ago • 5 comments

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

rderimay avatar Apr 18 '25 21:04 rderimay

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

rderimay avatar Apr 19 '25 08:04 rderimay

Even the track itself is displayed wrongly 95% of the time like this:

Image

and correctly sometimes:

Image

rderimay avatar Apr 19 '25 11:04 rderimay

Not resolved by v11.12.0-rc.1

rderimay avatar Apr 24 '25 08:04 rderimay

Hi @rderimay, could you provide some codes on how you setup CustomLayerHost?

maios avatar Jun 09 '25 12:06 maios

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
    }
}

rderimay avatar Jun 09 '25 13:06 rderimay