how to create a disc-shaped light source in SceneKit using Swift

871 views Asked by At

I am looking to create a light source that will resemble the way the Sun lights up the Earth using SceneKit. This is for an amateur astronomy course project. Since the Sun is larger than the Earth, an omnidirectional SCNLight will not do, since the light from it emanates from a single point. The light emanating from the Sun essentially emanates from a very large sphere, not a single point.

This image was created using an omnidirectional light source, but does not realistically show the Earth's shaded area. Specifically, the north pole is not lit, but it should be (in this case we are in summer. picture

This second image is more realistic, you can see the north pole is illuminated, as it would indeed be in the summer.

enter image description here

The problem is that to get the second image, I had to position a directional light very tediously to get the image to be correct (which I did manually. For the first image, I simply positioned an omnidirectional light at the same place as the sun sphere). Since the whole thing is intended to be animated, using a directional light and having to reposition it constantly as Earth progresses along its orbit, will require some fairly -for me- complicated math.

So I was thinking to create a SCNLight initialized using a model I/O light object created programmatically. According to Apple's "Documentation", in Swift, an SCNLight can be created with an initializer from a specified model I/O light object, which I understood to allow creation of a "light source [that] illuminates a scene in all directions from an area in the shape of a disc"

The "Documentation" states the following for "creating a light":

init(mdlLight: MDLLight)

it is defined as a convenience initializer:

convenience init(mdlLight: MDLLight)

I was expecting to be able to do the following:

let lightModelObject = MDLLight()
lightModelObject.lightType = .discArea

let discShapedSceneLight = SCNLight(lightModelObject) //cannot convert value ... error.

But the last statement gets me a: "Cannot convert value of type 'MDLLight' to expected argument type 'NSCoder'" error. I have also tried:

let discShapedSceneLight = SCNLight.init(lightModelObject)

but no luck there either.

I am totally stuck! It feels like there is something fundamental that I don't understand about using initializers in Swift.

Any comments would be much appreciated.

EDIT: I also tried the following in objective-C, as per the same documentation:

#import <ModelIO/MDLLight.h>
...
SCNLight *discLight = [SCNLight light];
MDLPhysicallcPlausibleLight *ppl = [MDLPhysicallyPlausibleLight lightWithSCNLight:discLight];

but get this error: "No known class method for selector 'lightWithSCNLight:'"

EDIT: Thanks to @EmilioPelaez for solving this.

I've put the complete code with the desired lighting below, perhaps it will help someone else.

import UIKit
import QuartzCore
import SceneKit
import SceneKit.ModelIO

class GameViewController: UIViewController {

    var scnView:SCNView!
    var scnScene:SCNScene!
    var cameraNode:SCNNode!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        setupScene()
        setupCamera()
        drawSolarSystem()
    }

    func setupView() {
        scnView = self.view as! SCNView
        scnView.showsStatistics = true
        scnView.allowsCameraControl = true
        scnView.autoenablesDefaultLighting = false
    }

    func setupScene() {
        scnScene = SCNScene()
        scnView.scene = scnScene
        scnScene.background.contents = UIColor.systemBlue
    }

    func setupCamera() {
        cameraNode = SCNNode()
        cameraNode.camera = SCNCamera()
        cameraNode.position = SCNVector3(x: 0, y: 0, z: 10)
        scnScene.rootNode.addChildNode(cameraNode)
    }

    func drawSolarSystem() {
      var geometryObject:SCNGeometry

//SUN object and light source
        let sunX:Float = -3.5   ///position a little off to the left of center

        //create the material for the Sun's surface
        let sunMaterial = SCNMaterial()
        sunMaterial.emission.contents = UIColor.yellow          ///the color the material emits.
//        sunMaterial.transparency = 0.3
//        sunMaterial.diffuse.contents = UIColor.systemYellow ///the color the material reflects when lit.

        //create the Sphere and assign it the material
        geometryObject = SCNSphere(radius: 1.0)
        geometryObject.firstMaterial=sunMaterial

        //create the node and assign it the geometry (object) previously created.
        let sunNode = SCNNode(geometry: geometryObject)
        sunNode.position = SCNVector3(x:sunX, y:0, z:0)
        scnScene.rootNode.addChildNode(sunNode)

        //create the light source and position it same place as the sun
        //create an MDLAreaLight, since the "normal" SCNLight types - such as omni - are not suitable.
        //The .omni type emanates from a point, and so doesn't correctly represent the sun lighting the earth
         let lightModelObject = MDLAreaLight()
         lightModelObject.lightType = .discArea
//         lightModelObject.areaRadius = 5.01 ///This doesn't appear to affect the light.

        //create the node and assign it the MDLAreaLight
         let sunLightNode = SCNNode()
         sunLightNode.light = SCNLight(mdlLight:lightModelObject)
         sunLightNode.light?.color = UIColor .white
         sunLightNode.position = SCNVector3(x:sunX, y:0, z:0)
         scnScene.rootNode.addChildNode(sunLightNode)


        //EARTH EQUATORIAL PLANE but centered on the Sun
        let floorObject = SCNFloor()
        floorObject.reflectivity = 0
        floorObject.width = 2
        floorObject.length = 3
        let earthEquatorialPlaneNode = SCNNode(geometry: floorObject)
        earthEquatorialPlaneNode.position = SCNVector3(x:sunX, y:0, z:0)
        scnScene.rootNode.addChildNode(earthEquatorialPlaneNode)


//EARTH main node - node with 2 subnodes, one sphere and one axis
        ///a node can only have a single geometry object attached. In order to attach multiple geometries, create a (parent) node without any geometry, and then attach subnodes with one geometry each.

        //The parent node
        let earthNode = SCNNode()
        earthNode.position = SCNVector3(x: 0, y:-1.2, z:0)
        scnScene.rootNode.addChildNode(earthNode)

        //the child node for the earth axis of rotation object
        geometryObject = SCNCylinder(radius: 0.01, height: 1.2)
        let earthAxisNode = SCNNode(geometry: geometryObject)
        earthNode.addChildNode(earthAxisNode)

        //the child node for the earth sphere object
        geometryObject = SCNSphere(radius: 0.5)
        let earthSphereNode = SCNNode(geometry: geometryObject)
        earthNode.addChildNode(earthSphereNode)

        //put some meridians and an equator onto the sphere.
        let earthSphereMaterial = SCNMaterial()
        geometryObject.firstMaterial = earthSphereMaterial
        earthSphereMaterial.diffuse.contents = "coordinateGrid.png"
        earthSphereMaterial.lightingModel = .lambert

    }

    override var shouldAutorotate: Bool {
        return true
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }

}
2

There are 2 answers

2
EmilioPelaez On BEST ANSWER

If you add import SceneKit.ModelIO you should be able to use the initializers that are currently not working.

1
mnuages On

Note that MDLLightTypeDiscArea is currently not bridged to SceneKit, what you'll get instead is an SCNLightTypeOmni light (you can verify that but inspecting the result of the initializer).