How to cast shadow of rotating fan blades?

In my app I want to show an animation of slowly rotating fan blades (four of them) in front of the fan body and background.


I can get close to the desired result by simply rotating the image of the blades in a UIImageView using Core Animation. But the blades look too "flat". I can get a bit closer still by calculating a slight shadow for the blades, and then rotating the shaded image just 90° over and over. That still doesn't look great.


            self.layer.shadowColor = UIColor.black.cgColor
            self.layer.shadowOpacity = 0.8
            self.layer.shadowOffset = CGSize(width: 1, height: 1.5)
            self.layer.shadowRadius = 2.5
            let animate = CABasicAnimation(keyPath: "transform.rotation")



So I've taken the time to play with SpriteKit enough to get close to a possible solution. I need a bit more help to complete the approach. I want the shadow of the blades to be seen on the background behind it. So far I can only get the blades to look like they're lit, but they are not casting a shadow.


Here's my excerpted code for the view controller where I want this to happen:


import SpriteKit


    func addShuriken() {  // example comes from raywunderlich, modified
        let shuriken = SKSpriteNode(imageNamed: "Fan Blades") // This is my png image of 4 fan blades
         let actualY = 300
        let left = arc4random() % 2
        let actualX = 150
        shuriken.position = CGPoint(x: 0.5, y: 0.5)
        shuriken.name = "shuriken"
        shuriken.zPosition = 1
        shuriken.lightingBitMask = 1
        shuriken.shadowCastBitMask = 1
        shuriken.size = CGSize(width: 60.0, height: 60.0)
        animationView.scene?.addChild(shuriken)
        let angle = left == 0 ? -CGFloat.pi/2 : CGFloat.pi/2
        let rotate = SKAction.repeatForever(SKAction.rotate(byAngle: angle, duration: 0.5))
        shuriken.run(SKAction.repeatForever(rotate))
    }

    func makeScene() -> SKScene {
        let minimumDimension = min(80, 80)
        let size = CGSize(width: minimumDimension, height: minimumDimension)
        let scene = SKScene(size: size)
        scene.backgroundColor = .blue  // I'd prefer this to be clear.  I think I'll probably have to put an image here on which to cast a shadow
        scene.alpha = 1.0
        scene.scaleMode = .resizeFill
        scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        animationView.allowsTransparency = true
        let light = SKLightNode()
        light.lightColor = .white
        light.shadowColor = .black
        light.falloff = 0.5
        light.isEnabled = true
        light.categoryBitMask = 1
        light.position = CGPoint(x: 0.2, y: 0.0)
        scene.addChild(light)
        return scene
    }
    private lazy var animationView = SKView()

    override func loadView() {
        super.loadView()
        view.addSubview(animationView)
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        guard animationView.scene == nil else {
            return
        }   
        let scene = makeScene()
        animationView.frame.size = scene.size
        animationView.presentScene(scene)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()   
        animationView.center.x = view.bounds.midX
        animationView.center.y = view.bounds.midY
    }


This code produces a rotating image with uniform lighting on the fan blades but no shadow cast onto the background. The lighting doesn't seem to be from one direction - it looks like it's centered. I'll attach a screen shot if you can tell me how to do that here.

Replies

Whoa, over 90 views and no replies. Was it something I said?


Is this something that's really hard to do, or so simple that nobody wants to help this noob along?

It's hard to diagnose issues like this from a forum post. Two things suggest themselves:


1. Your "shuriken" node has a zPosition of 1, which is not very "high" above the background. It's possible that the shadow exists but just isn't very visible. Think of a the shadow cast by a piece of paper that's floating 1/72nd of an inch above a table.


2. The documentation for SKLightNode suggests that a shadow will only appear on a node (so, a third node, not the light node or the shadow-caster node) if that node has a suitable "shadowedBitMask". In your example, nothing has this bit mask. (Or I'm mis-reading the documentation.)

Some reason you have two same topic threads on this?


Re: Can I shade an animated object?

Your comments got me thinking about the relative positions of light source, shadow-casting object, and background. One improvement was easily made by better positioning of the light away from dead center. This looks much better:

light.position = CGPoint(x: -10, y: 10)
light.zPosition = 400


But does the zPosition really matter? I can't see that it makes any difference.


My current problem seems to be very similar to the one described here:

ht tps://stackoverflow.com/questions/28919411/how-to-get-sun-at-noon-shadow-with-sklightnode


An answer there says "You can't achieve the effect you want with SKLightNode. Remember that SK is a 2D platform. Your desired effect is 3D."


Is that accurate? What do I need to do?

It's not quite true that Sprite Kit is 2D, since it has zPositions for its objects! The 2D-ness comes from the lack of rendering the appearance of depth (effects like perspective and foreshortening).


What I would expect is that the calculation of what light rays hit or don't hit a node depend on all 3 dimensions of their positions. A light immediately above a node isn't going to cast much illumination on anything except the node itself. A light far above a node lights most of the scene.


I think you should keep experimenting with relative x, y and z positions. Remember that (IIUC) the node showing the shadows is the SKScene (and the fan body, but you didn't show any code for that). So, again I ask what kind of shadow you would expect from a piece of paper lying on the ground, at noon if you want to be specific?

Does the .zPosition represent an actual distance, or just an ordering of layers? I've just read the documentation and it's still not clear to me. It seems more like it's just the order, and thus you cannot really place the light source higher or lower, only above or below the lit object. That would be consistent with my observations. I did read that a node's .zPoistion is realtive to its parent node. I need to check my code for the parents and children positions.

Yes, a node's zPosition is relative to its parent, but each node has an effective absolute zPosition, just as it has an effective absolute x-y position.


My understanding is that it represents an actual distance for the few things where the distance matters. In most cases (and in the original release of Sprite Kit several years ago, which did not have light or camera nodes) it amounts to a simple z ordering, because there is no perspective/foreshortening. However, in cases like the casting of light rays, which determines both light and shadow, I would expect it to be taken into account.


You could test this fairly easily. Try putting a light where it casts some visible shadow. Then starting increasing the light node's zPosition, and you should see the visible shadow get smaller relative to the node that casts the shadow. Note that this is all subject to the "fake" (i.e. not strictly physically motivated) effects that light nodes may have. Some effects may be generated purely algorithmically, or take shortcuts to speed performance.