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

Viewed 337

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?

1 Answers

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