Search code examples
iosswiftcamerascenekit

How do I position an SCNCamera such that an object "just fits" in view?


I am making a 3D board game using SceneKit. The board itself is flat, but the pieces on it are 3D. I want to position the camera such that it is looking down at the board at an angle like this:

                camera
               /
              /
             /
            /
           /
board     /
___________

I want to position the camera such that the entire board is in view, and not small. I know the board's dimensions at runtime, but not at compile time though, because the user can choose from many different boards to play the game.

I tried to calculate how far the camera will at least have to be to see the full width of the board. I drew a diagram like this:

enter image description here

and worked out that the distance is the half the width of the board divided by tan(FOV/2).

Translating that to code:

private func setupCamera(boardWidth: CGFloat, maxBoardZ: CGFloat, boardCenterX: CGFloat) {
    cameraNode = SCNNode()
    let camera = SCNCamera()
    cameraNode.camera = camera

    let boardRadius = boardWidth / 2
    let cameraDistance = boardRadius / tan(degreesToRadians(camera.fieldOfView) / 2)
    let cameraHeight: Float = 10

    cameraNode.position = SCNVector3(x: Float(boardCenterX), y: cameraHeight, z: Float(maxBoardZ + cameraDistance))

    cameraNode.eulerAngles.x = -0.5
}

cameraNode is a property of the scene to which I later set the scene view's pointOfView to.

This looks okay on portrait mode (this is only a portion of the whole phone screen):

enter image description here

The camera couldn't have moved much closer to the board without cutting off parts of the board.

But in landscape, the board is extremely small compared to the SCNView that it is in:

enter image description here

The camera could have moved a lot closer! I would have expected something like:

enter image description here

Notice the white background region - that's the SCNView.

I thought the FOV might have changed in landscape, but when I checked in the debugger, the FOV is always 60 degrees, no matter the orientation. It seems like even though SCNCamera has a fieldOfView property, SCNView also has its own, separate, FOV that depends on its width and height, and I have no idea how to access it.

How can I position the camera such that the whole board just fits inside the SCNView?


Solution

  • Ok - posting it here - hope that's ok.

    I used strafe just for a couple of big maps, but eventually took them out. I wanted all of the maps to fit - it looks very similar to yours. So yeah, I worked backwards. I put the camera where I wanted it, then fiddled with the map and panel size, so I was only dealing with one thing at a time.

    I had panels for triangles, quads, and hex shapes. It was a tower defense game, so the attackers could move various ways depending on the type of panel.

    class Camera
    {
        var data = Data.sharedInstance
        var util = Util.sharedInstance
        var gameDefaults = Defaults()
        
        var cameraEye = SCNNode()
        var cameraFocus = SCNNode()
            
        var centerX: Int = 100
        var strafeDelta: Float = 0.8
        var zoomLevel: Int = 35
        var zoomLevelMax: Int = 35              // Max number of zoom levels
        
        //********************************************************************
        init()
        {
            cameraEye.name = "Camera Eye"
            cameraFocus.name = "Camera Focus"
            
            cameraFocus.isHidden = true
            cameraFocus.position  =  SCNVector3(x: 0, y: 0, z: 0)
            
            cameraEye.camera = SCNCamera()
            cameraEye.constraints = []
            cameraEye.position = SCNVector3(x: 0, y: 15, z: 0.1)
            
            let vConstraint = SCNLookAtConstraint(target: cameraFocus)
            vConstraint.isGimbalLockEnabled = true
            cameraEye.constraints = [vConstraint]
        }
        //********************************************************************
        func reset()
        {
            centerX = 100
            cameraFocus.position  =  SCNVector3(x: 0, y: 0, z: 0)
            cameraEye.constraints = []
            cameraEye.position = SCNVector3(x: 0, y: 32, z: 0.1)
            cameraFocus.position = SCNVector3Make(0, 0, 0)
            
            let vConstraint = SCNLookAtConstraint(target: cameraFocus)
            vConstraint.isGimbalLockEnabled = true
            cameraEye.constraints = [vConstraint]
        }
        //********************************************************************
        func strafeRight()
        {
            if(centerX + 1 < 112)
            {
                centerX += 1
                cameraEye.position.x += strafeDelta
                cameraFocus.position.x += strafeDelta
            }
        }
        //********************************************************************
        func strafeLeft()
        {
            if(centerX - 1 > 90)
            {
                centerX -= 1
                cameraEye.position.x -= strafeDelta
                cameraFocus.position.x -= strafeDelta
            }
        }
        //********************************************************************
    }
    

    I used GKGraph like this:

    var graphNodes: [GKPanelNode] = [] // All active graph nodes with connections
    var myGraph = GKGraph()   // declaring the Graph
    

    Then your typical panel load, probably similar to yours

    func getQuadPanelNode(vPanelType: panelTypes) -> SCNNode
        {
            let plane = SCNBox(width: 1.4, height: 0.001, length: 1.4, chamferRadius: 0)
            
            plane.materials = []
            plane.materials = setQuadPanelTextures(vPanelType: vPanelType)
            plane.firstMaterial?.isDoubleSided = false
            return SCNNode(geometry: plane)
        }
    

    Then load the panels from the map I created.

    func loadPanels()
        {
            removePanelNodes()
            for vPanel in mapsDetail.getDetail(vMap: data.mapSelected)
            {
                let vPanel = Panel.init(vName: "Panel:" + vPanel.name, vPanelType: vPanel.type, vPosition: vPanel.pos, vRotation: vPanel.up)
                gridPanels[vPanel.panelName] = vPanel
                
                if(vPanel.type == .entry) { entryPanelName = vPanel.panelName }
                if(vPanel.type == .exit)  { exitPanelName  = vPanel.panelName }
            }
        }