swiftscenekitarkitscnnodescnscene

SCNFloor goes unvisible randomly on SceneKit


I get AR object from an API call and store then in the file manager storage, during this process, I merged all the nodes in the one node, and also add an SCNFloor below the object:

       let mergedScene = SCNScene()
            let nodeContainer = SCNNode()
            
            let floor = SCNNode()
            let scnFloor = SCNFloor()
            scnFloor.length = 2
            scnFloor.width = 2
            floor.geometry = scnFloor
            floor.geometry?.firstMaterial!.colorBufferWriteMask = []
            floor.geometry?.firstMaterial!.readsFromDepthBuffer = true
            floor.geometry?.firstMaterial!.writesToDepthBuffer = true
            floor.geometry?.firstMaterial!.lightingModel = .constant

            nodeContainer.addChildNode(floor)

            for node in scene.rootNode.childNodes {
                nodeContainer.addChildNode(node)
            }
            mergedScene.rootNode.addChildNode(nodeContainer)
            
            
            
            if mergedScene.write(to: url, options: nil, delegate: nil, progressHandler: { float, error, pointer in
                if let error = error {
                    completion(.error(error))
                }
            }) 

and here is the default lighting that I added in viewDidLoad

      let light = SCNLight()
        light.type = .directional
        light.shadowColor =  UIColor(white: 0, alpha: 0.6)
        light.color = UIColor.white
        light.castsShadow = true
        light.automaticallyAdjustsShadowProjection = true
        light.shadowMode = .deferred
        light.shadowRadius = 10
        light.shadowSampleCount = 3
        let sunLightNode = SCNNode()
        sunLightNode.position = SCNVector3(x: 1_000, y: 1_000, z: 0)
        sunLightNode.rotation = SCNVector4(x: 1, y: 0, z: 0, w: .pi * 1.5)
        sunLightNode.light = light
        light.orthographicScale = 5

        sceneView.scene.rootNode.addChildNode(sunLightNode)

And the configuration:

 sceneView.preferredFramesPerSecond = 30
        sceneView.automaticallyUpdatesLighting = true
        sceneView.autoenablesDefaultLighting = true
        let configuration = ARWorldTrackingConfiguration()
        configuration.isLightEstimationEnabled = false
        configuration.planeDetection = .horizontal
        configuration.providesAudioData = false
        configuration.isAutoFocusEnabled = true
        sceneView.session.run(configuration)

The problem is, maybe 4 out of 10 times, when you place the object in the scene, there is no shadow, or even when there is the shadows, maybe when you move camera away from the object and come back to the object, you will see the shadow is gone, it's not happened all the time but it happens quite a lot. But on the other times, when you place the object you can see the shadow it won't be gone by moving the camera!

And when I render a snapshot from sceneView when there is no shadow on the screen, you can see the shadow on the rendered snapshot, so the shadow is there, but only not visible on the screen! Could anyone help me to solve this issue? Thank you


Solution

  • First configure your SceneView Setup:

    Make sure to configure this:

    sceneView.antialiasingMode = .none
    sceneView.isJitteringEnabled = false
    sceneView.autoenablesDefaultLighting = false // we configure our own lights
    sceneView.automaticallyUpdatesLighting = false // we configure our own lights
    

    For the Floor:

    func shadowPlane(position: SCNVector3) -> SCNNode {
        
        let objectShape = SCNPlane(width: 100, height: 100)
        objectShape.heightSegmentCount = 1
        objectShape.widthSegmentCount = 1
        
        let objectNode = SCNNode(geometry: objectShape)
        objectNode.name = "floor"
        objectNode.renderingOrder = -10 // must be for Shadow Material Standard
        objectNode.position = position
        objectNode.geometry?.firstMaterial = Material.shadowMaterialStandard()
        objectNode.castsShadow = false // Important
        objectNode.eulerAngles = SCNVector3(-90.degreesToRadians, 0.0, 0.0)
        objectNode.physicsBody = Physics.floorPhysicsBody(shape: objectShape)
        
        return objectNode
    }
    

    For the Material:

    func shadowMaterialStandard() -> SCNMaterial {
        
        let material = SCNMaterial()
        
        material.colorBufferWriteMask = SCNColorMask(rawValue: 0)
        material.diffuse.contents = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
        
        material.lightingModel = .physicallyBased
        material.isLitPerPixel = false // play around with it (can affect performance)
        
        return material
    }
    

    For the Directional Light: (Add a SCNNode and configure its light property using this function. Do it for the directional and the ambient light)

    func directionalLight() -> SCNLight {
        
        let light = SCNLight()
        
        light.type = .directional
        light.castsShadow = true
        
        light.color = UIColor.white
        light.shadowMode = .deferred
        light.shadowSampleCount = 16 // 8
        light.shadowRadius = 1 // 1
        light.automaticallyAdjustsShadowProjection = true // seems to be required, if not, the shadow seems incomplete
        light.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.75)
        // light.shadowMapSize = CGSize(width: 4096, height: 4096) // CGSize(width: 8192, height: 8192) // CGSize(width: 4096, height: 4096) // increases Memory usage to 700 MB (be careful with this)
        light.categoryBitMask = -1 // shine on everything
        
        return light
    }
    

    and do something like this on the Node containing the directional Light:

    lightNode.eulerAngles = SCNVector3(-45.degreesToRadians, 0.0, 0.0)
    

    you need this extension:

    extension FloatingPoint {
        var degreesToRadians: Self { return self * .pi / 180 }
        var radiansToDegrees: Self { return self * 180 / .pi }
    }
    

    For the Ambient Light:

    func ambientLight() -> SCNLight {
    
        let light = SCNLight()
    
        light.type = .ambient
        light.intensity = 150
        light.color = UIColor.white
        light.categoryBitMask = -1 // shine on everything
    
        return light
    }
    

    Note: try to place the Light Setup first, then the Floor and then the Geometry on the Floor, or whatever.

    and I highly recommend you to NOT configure this setting you already made:

    sceneView.preferredFramesPerSecond = 30
    

    I never had any success with it. It can also cause some unexpected frame stuttering.