SpriteKit: sprite looks blurry (with ghosting) at high velocity but fine at low velocity

Viewed 488

When using high velocity (linear or angular) in SpriteKit, sprites look blurry as if there are "ghosts" trailing the sprite. The sprite looks fine at slow speeds.

Below is a screenshot and GIF illustrating the blurriness/ghosting problem with high linear velocity, but the problem also occurs with the angularVelocity property.

Ball Code (use SKScene below to reproduce blurriness):

    let radius = CGFloat(8)

    let body = SKPhysicsBody(circleOfRadius: radius)
    body.isDynamic = true
    body.affectedByGravity = false
    body.allowsRotation = true
    body.friction = 0
    body.restitution = 0.0
    body.linearDamping = 0.0
    body.angularDamping = 0
    body.categoryBitMask = categoryBitMask

    let ball = SKShapeNode(circleOfRadius: radius)
    ball.physicsBody = body
    ball.physicsBody?.velocity.dx = 0
    ball.physicsBody?.velocity.dy = -1200

Looks fine:

ball.physicsBody?.velocity.dy = -200

Looks blurry:

ball.physicsBody?.velocity.dy = -1200

Screenshot:

enter image description here

GIF:

enter image description here

SKScene (drop in project and present scene to see blurriness):

import Foundation
import SpriteKit


class TestScene : SKScene, SKPhysicsContactDelegate {
    let BallBitMask                   : UInt32 = 0x1 << 1
    let BottomWallBitMask             : UInt32 = 0x1 << 3
    let TopWallBitMask                : UInt32 = 0x1 << 4
    let RightWallBitMask              : UInt32 = 0x1 << 5
    let LeftWallBitMask               : UInt32 = 0x1 << 6

    let SceneBackgroundColor = UIColor(red: 58/255.0, green: 50/255.0, blue: 96/255.0, alpha: 1.0)

    let HorizontalWallHeight = CGFloat(10)
    let VerticallWallWidth = CGFloat(5)

    override init() {
        super.init()
    }


    override init(size: CGSize) {
        super.init(size: size)
        doInit()
    }


    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }


    fileprivate func doInit() {
        // Set background
        backgroundColor = SceneBackgroundColor

        // Set scale mode
        scaleMode = .resizeFill

        // Set anchor point to screen center
        anchorPoint = CGPoint(x: 0.5, y: 0.5)

        // Add walls
        layoutWalls()

        // Create ball
        let radius = CGFloat(8)
        let body = SKPhysicsBody(circleOfRadius: radius)
        body.isDynamic = true
        body.affectedByGravity = false
        body.allowsRotation = true
        body.friction = 0
        body.restitution = 0.0
        body.linearDamping = 0.0
        body.angularDamping = 0
        body.categoryBitMask = BallBitMask
        body.collisionBitMask =  TopWallBitMask | RightWallBitMask | BottomWallBitMask | LeftWallBitMask

        let ball = SKShapeNode(circleOfRadius: radius)
        ball.fillColor = UIColor.orange
        ball.physicsBody = body
        ball.physicsBody?.velocity.dx = 0
        ball.physicsBody?.velocity.dy = -1200

        // Add ball to scene
        addChild(ball)
    }


    fileprivate func layoutWalls() {
        // Set wall offset
        let wallOffset = CGFloat(3)

        // Layout bottom wall
        let bottomWallSize = CGSize(width: UIScreen.main.bounds.width, height: HorizontalWallHeight)
        let bottomWall = SKSpriteNode(color: UIColor.red, size: bottomWallSize)
        bottomWall.position.y = -UIScreen.main.bounds.height/2 - bottomWallSize.height/2 - wallOffset
        bottomWall.physicsBody = createWallPhysics(categoryBitMask: BottomWallBitMask, wallSize: bottomWallSize)
        addChild(bottomWall)

        // Layout top wall
        let topWallSize = CGSize(width: UIScreen.main.bounds.width, height: HorizontalWallHeight)
        let topWall = SKSpriteNode(color: UIColor.red, size: topWallSize)
        topWall.position.y = UIScreen.main.bounds.height/2 + topWallSize.height/2 + wallOffset
        topWall.physicsBody = createWallPhysics(categoryBitMask: TopWallBitMask, wallSize: topWallSize)
        addChild(topWall)

        // Layout right wall
        let rightWallSize = CGSize(width: VerticallWallWidth, height: UIScreen.main.bounds.height)
        let rightWall = SKSpriteNode(color: UIColor.blue, size: rightWallSize)
        rightWall.position.x = UIScreen.main.bounds.width/2 + rightWallSize.width/2 + wallOffset
        rightWall.physicsBody = createWallPhysics(categoryBitMask: RightWallBitMask, wallSize: rightWallSize)
        addChild(rightWall)

        // Layout left wall
        let leftWallSize = CGSize(width: VerticallWallWidth, height: UIScreen.main.bounds.height)
        let leftWall = SKSpriteNode(color: UIColor.blue, size: leftWallSize)
        leftWall.position.x = -UIScreen.main.bounds.width/2 - leftWallSize.width/2 - wallOffset
        leftWall.physicsBody = createWallPhysics(categoryBitMask: LeftWallBitMask, wallSize: leftWallSize)
        addChild(leftWall)
    }


    fileprivate func createWallPhysics(categoryBitMask: UInt32, wallSize: CGSize) -> SKPhysicsBody {
        // Create new physics body for wall
        let physicsBody = SKPhysicsBody(edgeLoopFrom: CGRect(x: -wallSize.width/2, y: -wallSize.height/2, width: wallSize.width, height: wallSize.height))
        physicsBody.isDynamic = true
        physicsBody.friction = 0
        physicsBody.restitution = 1.0
        physicsBody.linearDamping = 0
        physicsBody.angularDamping = 0.0
        physicsBody.categoryBitMask = categoryBitMask

        // Return body
        return physicsBody
    }
}
1 Answers

Which one of these looks more ghosty?

The "trick" is being performed by the eye. We're not equipped to deal with screens at a lowly 60fps with fast moving objects. We sustain an image on the screen and in position through a faux persistence of vision so our brains and consciousness can figure out how fast something is "moving" on the screen.

In real life we get a near infinite number of "frames" to process movement with, and depth and all sorts of other cues, so we rarely do this anywhere near as much.

We still do it, but it's much less perceptible because we've got that near infinite number of frames to call on.

The below three images do different things to reveal this in different ways.

The first one is linear speed, accelerates instantly to its velocity of rotation and stops instantly.

The second has a ramp up and ramp down to its rotational speed, which has a higher peak speed of rotation. This has an interesting effect on the brain that permits it to prepare for the velocity that's going to be achieved.

The final has a lot of fake motion blur (too much for real world motion graphics usage) that shows how effective blur is at solving the effect of this problem, and why slow shutter speeds are so incredibly important to movie making.

Linear rotation rate: enter image description here

Accel and decel:

enter image description here

Heavily blurred:

enter image description here

Related