Skip to content

Custom Style Layers (Metal API)

Creating a Custom Style Layer with Metal

Custom style layers allow you to draw directly with Metal, enabling you to render specialized shapes, custom geometry, or apply advanced visual effects that go beyond what is possible with standard style layers.

Below you can find an example of how to create a custom style layer with MLNCustomStyleLayer. In this implementation, a SwiftUI view wraps an MLNMapView and appends a subclassed custom style layer once the map loads. The layer’s MLNCustomStyleLayer/didMoveToMapView: method handles initialization, including compiling Metal shaders and creating a MTLRenderPipelineState for subsequent draw operations. The MLNCustomStyleLayer/willMoveFromMapView: method provides a place to release or invalidate resources when the layer is removed from the map, while the MLNCustomStyleLayer/drawInMapView:withContext: method encodes the drawing commands using a MTLRenderCommandEncoder and the map’s projection matrix. By projecting latitude/longitude coordinates into a normalized 0–1 space and then transforming them into tile coordinates, the layer ensures that rendered geometry aligns correctly with the base map.

swift
struct CustomStyleLayerExample: UIViewRepresentable {
    func makeCoordinator() -> CustomStyleLayerExample.Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, MLNMapViewDelegate {
        var control: CustomStyleLayerExample

        init(_ control: CustomStyleLayerExample) {
            self.control = control
        }

        func mapViewDidFinishLoadingMap(_ mapView: MLNMapView) {
            let mapOverlay = CustomStyleLayer(identifier: "test-overlay")
            let style = mapView.style!
            style.layers.append(mapOverlay)
        }
    }

    func makeUIView(context: Context) -> MLNMapView {
        let mapView = MLNMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_: MLNMapView, context _: Context) {}
}

class CustomStyleLayer: MLNCustomStyleLayer {
    private var pipelineState: MTLRenderPipelineState?
    private var depthStencilStateWithoutStencil: MTLDepthStencilState?

    override func didMove(to mapView: MLNMapView) {
        #if MLN_RENDER_BACKEND_METAL
            let resource = mapView.backendResource()

            let shaderSource = """
            #include <metal_stdlib>
            using namespace metal;

            typedef struct
            {
               vector_float2 position;
               vector_float4 color;
            } Vertex;

            struct RasterizerData
            {
               float4 position [[position]];
               float4 color;
            };

            struct Uniforms
            {
               float4x4 matrix;
            };

            vertex RasterizerData
            vertexShader(uint vertexID [[vertex_id]],
                        constant Vertex *vertices [[buffer(0)]],
                        constant Uniforms &uniforms [[buffer(1)]])
            {
               RasterizerData out;

               const float4 position = uniforms.matrix * float4(float2(vertices[vertexID].position.xy), 1, 1);

               out.position = position;
               out.color = vertices[vertexID].color;

               return out;
            }

            fragment float4 fragmentShader(RasterizerData in [[stage_in]])
            {
               return in.color;
            }
            """

            var error: NSError?
            let device = resource.device
            let library = try? device?.makeLibrary(source: shaderSource, options: nil)
            assert(library != nil, "Error compiling shaders: \(String(describing: error))")
            let vertexFunction = library?.makeFunction(name: "vertexShader")
            let fragmentFunction = library?.makeFunction(name: "fragmentShader")

            // Configure a pipeline descriptor that is used to create a pipeline state.
            let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
            pipelineStateDescriptor.label = "Simple Pipeline"
            pipelineStateDescriptor.vertexFunction = vertexFunction
            pipelineStateDescriptor.fragmentFunction = fragmentFunction
            pipelineStateDescriptor.colorAttachments[0].pixelFormat = resource.mtkView.colorPixelFormat
            pipelineStateDescriptor.depthAttachmentPixelFormat = .depth32Float_stencil8
            pipelineStateDescriptor.stencilAttachmentPixelFormat = .depth32Float_stencil8

            do {
                pipelineState = try device?.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
            } catch {
                assertionFailure("Failed to create pipeline state: \(error)")
            }

            // Notice that we don't configure the stencilTest property, leaving stencil testing disabled
            let depthStencilDescriptor = MTLDepthStencilDescriptor()
            depthStencilDescriptor.depthCompareFunction = .always // Or another value as needed
            depthStencilDescriptor.isDepthWriteEnabled = false

            depthStencilStateWithoutStencil = device!.makeDepthStencilState(descriptor: depthStencilDescriptor)
        #endif
    }

    override func willMove(from _: MLNMapView) {}

    override func draw(in _: MLNMapView, with context: MLNStyleLayerDrawingContext) {
        #if MLN_RENDER_BACKEND_METAL
            guard let renderEncoder else { return }

            // Project to 0..1.
            let p1 = project(CLLocationCoordinate2D(latitude: 25.0, longitude: 12.5))
            let p2 = project(CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0))
            let p3 = project(CLLocationCoordinate2D(latitude: 0.0, longitude: 25.0))

            // Multiply by the world size so it becomes the tile coordinate system.
            let worldSize = 512.0 * pow(2.0, context.zoomLevel)

            let p1Tile = CGPoint(x: p1.x * worldSize, y: p1.y * worldSize)
            let p2Tile = CGPoint(x: p2.x * worldSize, y: p2.y * worldSize)
            let p3Tile = CGPoint(x: p3.x * worldSize, y: p3.y * worldSize)

            // Then build a triangle from tile coordinates
            struct Vertex { var position: vector_float2; var color: vector_float4 }
            let triangleVertices: [Vertex] = [
                Vertex(position: vector_float2(Float(p1Tile.x), Float(p1Tile.y)),
                       color: vector_float4(1, 0, 0, 1)),
                Vertex(position: vector_float2(Float(p2Tile.x), Float(p2Tile.y)),
                       color: vector_float4(0, 1, 0, 1)),
                Vertex(position: vector_float2(Float(p3Tile.x), Float(p3Tile.y)),
                       color: vector_float4(0, 0, 1, 1)),
            ]

            // Use the camera's full projection matrix *unchanged*.
            var matrix = convertMatrix(context.projectionMatrix)

            // Encode
            renderEncoder.setRenderPipelineState(pipelineState!)
            renderEncoder.setDepthStencilState(depthStencilStateWithoutStencil)
            renderEncoder.setVertexBytes(triangleVertices, length: MemoryLayout<Vertex>.size * triangleVertices.count, index: 0)
            renderEncoder.setVertexBytes(&matrix, length: MemoryLayout<float4x4>.size, index: 1)

            // Draw the triangle.
            renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3)
        #endif
    }

    func project(_ coordinate: CLLocationCoordinate2D) -> CGPoint {
        // We project the coordinates into the space 0 to 1 and then scale these when drawing based on the current zoom level
        let worldSize = 1.0
        let x = (180.0 + coordinate.longitude) / 360.0 * worldSize
        let yi = log(tan((45.0 + coordinate.latitude / 2.0) * Double.pi / 180.0))
        let y = (180.0 - yi * (180.0 / Double.pi)) / 360.0 * worldSize

        return CGPoint(x: x, y: y)
    }

    struct MLNMatrix4f {
        var m00, m01, m02, m03: Float
        var m10, m11, m12, m13: Float
        var m20, m21, m22, m23: Float
        var m30, m31, m32, m33: Float
    }

    func convertMatrix(_ mat: MLNMatrix4) -> MLNMatrix4f {
        MLNMatrix4f(
            m00: Float(mat.m00), m01: Float(mat.m01), m02: Float(mat.m02), m03: Float(mat.m03),
            m10: Float(mat.m10), m11: Float(mat.m11), m12: Float(mat.m12), m13: Float(mat.m13),
            m20: Float(mat.m20), m21: Float(mat.m21), m22: Float(mat.m22), m23: Float(mat.m23),
            m30: Float(mat.m30), m31: Float(mat.m31), m32: Float(mat.m32), m33: Float(mat.m33)
        )
    }
}