Skip to content

Commit

Permalink
Example: use SceneKit in custom layer (#154)
Browse files Browse the repository at this point in the history
* SceneKit in custom layer

Port "antenna building" example from gl-js https://docs.mapbox.com/mapbox-gl-js/example/add-3d-model/

* Gesture lag and review fixes.

* Simplification. Review comments fixes.

* Rebasing. Review comments.
  • Loading branch information
astojilj authored Mar 17, 2021
1 parent 5946d75 commit 9804c12
Show file tree
Hide file tree
Showing 4 changed files with 1,124 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Examples/Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
0333B84F25ED942600D667C9 /* SceneKitExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0333B84E25ED942600D667C9 /* SceneKitExample.swift */; };
03BB33F925EDA19200109B28 /* 34M_17.dae in Resources */ = {isa = PBXBuildFile; fileRef = 03BB33F825EDA19200109B28 /* 34M_17.dae */; };
0706C4A625B1181A008733C0 /* TerrainExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0706C4A525B1181A008733C0 /* TerrainExample.swift */; };
073475D725AFAE520049B0B8 /* CustomLayerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073475D625AFAE520049B0B8 /* CustomLayerExample.swift */; };
077C4EFA252F7E89007636F1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 077C4EF8252F7E89007636F1 /* LaunchScreen.storyboard */; };
Expand Down Expand Up @@ -103,6 +105,8 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
0333B84E25ED942600D667C9 /* SceneKitExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneKitExample.swift; sourceTree = "<group>"; };
03BB33F825EDA19200109B28 /* 34M_17.dae */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml.dae; name = 34M_17.dae; path = ../../../../../Downloads/34M_17.dae; sourceTree = "<group>"; };
0706C4A525B1181A008733C0 /* TerrainExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerrainExample.swift; sourceTree = "<group>"; };
072C79EA25685D23006E47A7 /* SnapshotterExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotterExample.swift; sourceTree = "<group>"; };
073475D625AFAE520049B0B8 /* CustomLayerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomLayerExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -249,6 +253,7 @@
A4211CE52592549900D215B4 /* RestrictCoordinateBoundsExample.swift */,
0CE8ED8B2589285C0066E56C /* ModelExample.swift */,
073475D625AFAE520049B0B8 /* CustomLayerExample.swift */,
0333B84E25ED942600D667C9 /* SceneKitExample.swift */,
0C52BA9725AF8C880054ECA8 /* PuckModelLayerExample.swift */,
0706C4A525B1181A008733C0 /* TerrainExample.swift */,
0CC4ECE925B8AD3000F998B8 /* CustomLocationIndicatorLayerExample.swift */,
Expand Down Expand Up @@ -338,6 +343,7 @@
07B071D12547CF50007F2865 /* Sample Data */ = {
isa = PBXGroup;
children = (
03BB33F825EDA19200109B28 /* 34M_17.dae */,
07B071D22547CFC3007F2865 /* GeoJSONSourceExample.geojson */,
0CE8ED87258928200066E56C /* race_car_model.gltf */,
0C52BA9B25AFB5940054ECA8 /* arrow.gltf */,
Expand Down Expand Up @@ -465,6 +471,7 @@
buildActionMask = 2147483647;
files = (
07B071D32547CFC3007F2865 /* GeoJSONSourceExample.geojson in Resources */,
03BB33F925EDA19200109B28 /* 34M_17.dae in Resources */,
0C78AC2F25BF72C70057F570 /* GradientLine.geojson in Resources */,
0C52BA9C25AFB5940054ECA8 /* arrow.gltf in Resources */,
07DC84422538B1F100F4AF14 /* Assets.xcassets in Resources */,
Expand Down Expand Up @@ -560,6 +567,7 @@
CAC195B725AC098A00F69FEA /* CameraUIViewAnimationExample.swift in Sources */,
0C78AC2925BF70E40057F570 /* LineGradientExample.swift in Sources */,
CADCF72B2584990E0065C51B /* FlyToExample.swift in Sources */,
0333B84F25ED942600D667C9 /* SceneKitExample.swift in Sources */,
CADCF72D2584990E0065C51B /* GeoJSONSourceExample.swift in Sources */,
CADCF72E2584990E0065C51B /* MapViewExample.swift in Sources */,
58A3C0C925C4B93600CAE5F0 /* AnimateGeoJSONLineExample.swift in Sources */,
Expand Down Expand Up @@ -773,6 +781,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_WORKSPACE = YES;
};
name = Release;
};
Expand Down
882 changes: 882 additions & 0 deletions Examples/Examples/All Examples/Sample Data/34M_17.dae

Large diffs are not rendered by default.

230 changes: 230 additions & 0 deletions Examples/Examples/All Examples/SceneKitExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import UIKit
import SceneKit
import MapboxMaps

@objc(SceneKitExample)

public class SceneKitExample: UIViewController, ExampleProtocol, CustomLayerHost {

internal var mapView: MapView!
public var peer: MBXPeerWrapper?

let modelOrigin = CLLocationCoordinate2D(latitude: -35.39847, longitude: 148.9819)
var renderer: SCNRenderer!
var scene: SCNScene!
var modelNode: SCNNode!
var cameraNode: SCNNode!
var textNode: SCNNode!
var useCPUOcclusion = false

override public func viewDidLoad() {
super.viewDidLoad()

self.mapView = MapView(with: view.bounds, resourceOptions: resourceOptions())
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.addSubview(mapView)
mapView.cameraManager.setCamera(
centerCoordinate: self.modelOrigin,
zoom: 18,
bearing: 180,
pitch: 60
)
self.mapView.update { (mapOptions) in
mapOptions.render.presentsWithTransaction = true
}

self.mapView.on(.styleLoadingFinished) { [weak self] _ in
self?.addModelAndTerrain()
}
}

override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}

func addModelAndTerrain() {
try! mapView.__map.addStyleCustomLayer(forLayerId: "Custom",
layerHost: self,
layerPosition: LayerPosition(above: nil, below: "waterway-label", at: nil))

var demSource = RasterDemSource()
demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
demSource.tileSize = 512
demSource.maxzoom = 14.0
mapView.style.addSource(source: demSource, identifier: "mapbox-dem")
let terrain = Terrain(sourceId: "mapbox-dem")
_ = self.mapView.style.setTerrain(terrain)

var skyLayer = SkyLayer(id: "sky-layer")
skyLayer.paint?.skyType = .constant(.atmosphere)
skyLayer.paint?.skyAtmosphereSun = .constant([0, 0])
skyLayer.paint?.skyAtmosphereSunIntensity = .constant(15.0)

_ = self.mapView.style.addLayer(layer: skyLayer)

// Re-use terrain source for hillshade
let map = self.mapView.__map!
let properties = [
"id": "terrain_hillshade",
"type": "hillshade",
"source": "mapbox-dem",
"hillshade-illumination-anchor": "map"
] as [ String: Any ]

try! map.addStyleLayer(forProperties: properties,
layerPosition: LayerPosition(above: nil, below: "water", at: nil))
}

public func renderingWillStart(_ metalDevice: MTLDevice, colorPixelFormat: UInt, depthStencilPixelFormat: UInt) {
renderer = SCNRenderer(device: metalDevice)
scene = SCNScene()
renderer.scene = scene

modelNode = SCNScene(named: "34M_17")?.rootNode.clone()
scene.rootNode.addChildNode(modelNode)

cameraNode = SCNNode()
let camera = SCNCamera()
cameraNode.camera = camera
camera.usesOrthographicProjection = false
scene.rootNode.addChildNode(cameraNode)
renderer.pointOfView = cameraNode
self.setupLight()
// In order to use depth occlusion, align with gl-native Z handling (doesn't use reverse Z).
if #available(iOS 13.0, *) {
renderer.usesReverseZ = false
} else {
// Fallback on earlier versions, disable depth in render()
self.useCPUOcclusion = true
}
}

func setupLight() {
// Ambient light
let ambientLight = SCNNode()
ambientLight.light = SCNLight()
ambientLight.light?.type = SCNLight.LightType.ambient
ambientLight.light?.color = UIColor(white: 0.4, alpha: 1.0)
modelNode.addChildNode(ambientLight)

let lightNode = SCNNode()
lightNode.light = SCNLight()
lightNode.light?.type = SCNLight.LightType.directional
lightNode.light?.orthographicScale = 30
lightNode.light?.color = UIColor(white: 0.8, alpha: 1.0)
lightNode.position = SCNVector3Make(-50, 100, 100)
lightNode.light?.zNear = 1
lightNode.light?.zFar = 1000
lightNode.light?.intensity = 2000
lightNode.look(at: modelNode.worldPosition)
modelNode.addChildNode(lightNode)
let pointNode = SCNNode()
pointNode.light = SCNLight()
pointNode.light?.type = SCNLight.LightType.omni
pointNode.light?.intensity = 3000
pointNode.position = SCNVector3Make(0, 25, 0)
modelNode.addChildNode(pointNode)
}

internal func makeTranslationMatrix(tx: Double, ty: Double, tz: Double) -> simd_double4x4 {
var matrix = matrix_identity_double4x4

matrix[3, 0] = tx
matrix[3, 1] = ty
matrix[3, 2] = tz

return matrix
}

internal func makeScaleMatrix(xScale: Double, yScale: Double, zScale: Double) -> simd_double4x4 {
var matrix = matrix_identity_double4x4

matrix[0, 0] = xScale
matrix[1, 1] = yScale
matrix[2, 2] = zScale

return matrix
}

public func render(_ parameters: CustomLayerRenderParameters, mtlCommandBuffer: MTLCommandBuffer, mtlRenderPassDescriptor: MTLRenderPassDescriptor) {
guard let colorTexture = mtlRenderPassDescriptor.colorAttachments[0].texture else {
return
}
let m = parameters.projectionMatrix

// It is essential to use double precision for computation below: using simd instead
// of SceneKit matrix operations.
var transformSimd = matrix_identity_double4x4
transformSimd[0, 0] = m[0].doubleValue
transformSimd[0, 1] = m[1].doubleValue
transformSimd[0, 2] = m[2].doubleValue
transformSimd[0, 3] = m[3].doubleValue
transformSimd[1, 0] = m[4].doubleValue
transformSimd[1, 1] = m[5].doubleValue
transformSimd[1, 2] = m[6].doubleValue
transformSimd[1, 3] = m[7].doubleValue
transformSimd[2, 0] = m[8].doubleValue
transformSimd[2, 1] = m[9].doubleValue
transformSimd[2, 2] = m[10].doubleValue
transformSimd[2, 3] = m[11].doubleValue
transformSimd[3, 0] = m[12].doubleValue
transformSimd[3, 1] = m[13].doubleValue
transformSimd[3, 2] = m[14].doubleValue
transformSimd[3, 3] = m[15].doubleValue

// Model is using metric unit system: scale x and y from meters to mercator and keep z is in meters.
let meterInMercatorCoordinateUnits = try! 1.0 / (Projection.getMetersPerPixelAtLatitude(forLatitude: modelOrigin.latitude, zoom: parameters.zoom))
let modelScale = makeScaleMatrix(xScale: meterInMercatorCoordinateUnits, yScale: -meterInMercatorCoordinateUnits, zScale: 1)

// Translate scaled model to model origin (in web mercator coordinates) and elevate to model origin's altitude (in meters).
let origin = try! Projection.project(for: modelOrigin, zoomScale: pow(2, parameters.zoom))
var elevation = 0.0
if let elevationData = parameters.elevationData, let elevationValue = elevationData.getElevationFor(self.modelOrigin) {
elevation = elevationValue.doubleValue
}
let translateModel = makeTranslationMatrix(tx: origin.x, ty: origin.y, tz: elevation)

let transform = transformSimd * translateModel * modelScale

let scnMat = SCNMatrix4(
m11: Float(transform[0, 0]),
m12: Float(transform[0, 1]),
m13: Float(transform[0, 2]),
m14: Float(transform[0, 3]),
m21: Float(transform[1, 0]),
m22: Float(transform[1, 1]),
m23: Float(transform[1, 2]),
m24: Float(transform[1, 3]),
m31: Float(transform[2, 0]),
m32: Float(transform[2, 1]),
m33: Float(transform[2, 2]),
m34: Float(transform[2, 3]),
m41: Float(transform[3, 0]),
m42: Float(transform[3, 1]),
m43: Float(transform[3, 2]),
m44: Float(transform[3, 3])
)
cameraNode.camera!.projectionTransform = scnMat

// flush automatic SceneKit transaction as SceneKit animation is not running and
// there's need to use transform matrix in this frame (not to have it used with delay).
SCNTransaction.flush()

if self.useCPUOcclusion {
mtlRenderPassDescriptor.depthAttachment = nil
mtlRenderPassDescriptor.stencilAttachment = nil
// Example uses depth buffer to occlude model when e.g. behind the hill.
// If depth buffer (SCNRenderer.usesReverseZ = false) is not available, or if wished to
// to indicate that model is occluded or e.g. implement fade out / fade in model occlusion,
// the example here needs to provide CPU side occlusion implementation, too.
// TODO: this is blocked on https://github.com/mapbox/mapbox-maps-ios/issues/155
}
renderer.render(withViewport: CGRect(x: 0, y: 0, width: CGFloat(colorTexture.width), height: CGFloat(colorTexture.height)), commandBuffer: mtlCommandBuffer, passDescriptor: mtlRenderPassDescriptor)

}

public func renderingWillEnd() {
// The below line is used for internal testing purposes only.
self.finish()
}
}
3 changes: 3 additions & 0 deletions Examples/Examples/Models/Examples.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ public struct Examples {
Example(title: "Show 3D terrain",
description: "Show realistic elevation by enabling terrain.",
type: TerrainExample.self),
Example(title: "SceneKit rendering on map",
description: "Use custom layer to render SceneKit model over terrain.",
type: SceneKitExample.self),
Example(title: "Customize the location puck",
description: "Use a different asset to represent the puck.",
type: CustomLocationIndicatorLayerExample.self),
Expand Down

0 comments on commit 9804c12

Please sign in to comment.