4

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
    }
}
Crashalot
  • 33,605
  • 61
  • 269
  • 439
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/159408/discussion-on-question-by-crashalot-spritekit-sprite-looks-blurry-with-ghostin). – Andy Nov 20 '17 at 17:42

1 Answers1

2

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

Confused
  • 6,048
  • 6
  • 34
  • 75
  • sorry, I can't get these to loop in SO. Very annoying. Save to HD and use mac preview. Just pressing space bar, they'll loop in that quick preview... – Confused Nov 17 '17 at 09:18
  • top one? though they are similar. so you have the same issue? – Crashalot Nov 17 '17 at 09:55
  • if you download the breakout tutorial from ray wenderlich this ghosting issue doesn’t exist for the ball so it’s definirely something we are doing wrong. – Crashalot Nov 17 '17 at 09:56
  • I've added a third image, see if it's noticeable better/different? @Crashalot – Confused Nov 17 '17 at 10:06
  • yah last one is best but still not smooth. did you try the breakout tutorial? – Crashalot Nov 17 '17 at 10:16
  • Got no xcode... so can't try the breakout tutorial. I think visual persistence is the issue. Having dinner with family, will get back to yo later on this... – Confused Nov 17 '17 at 10:27
  • what are you coding in, if not xcode? yes, the issue is visual persistence. so frustrating this is not simple in spritekit! – Crashalot Nov 17 '17 at 10:34
  • Ok, visual persistent is something your eyes and brain do. You can't prevent this, and the frequencies are different for every person. It's something you can work around, though. You can make fake blurs, that's what I did in that 3rd image above. If you download it, and step through the frames, you'll see what I did. I've created an (exaggerated) version of what our brain wants to see when something moves fast. The best way to get a good result is to lower the contrast and increase the size of objects, if you have to keep the speed the same, and then add camera moves that help the viewer. – Confused Nov 17 '17 at 11:09
  • If you lerp the camera in a way that moves the background more than the ball, you'll be helping the viewer perceive motion without need to actually suffer the limitations of a slow frame rate (60fps is slow for us) where objects step so far from frame to frame that our brains create excessive visual persistence to try reconcile what we expect should be happening in our vision. – Confused Nov 17 '17 at 11:11
  • 1
    @Crashalot On coding, I'm trying out Unity, Godot, Game Maker Studio, Codea and a couple of other things... trying to find what I like best. I decided I don't like Sprite Kit and that it reminds me of all the things I don't like about coding. That Apple makes it kind of ridiculous to use doesn't help. So much code, so little happening. I like Swift, and Core Animation, but SpriteKit is intolerably under-designed, ill-conceived and a legacy of everything wrong with the way apple treats coders, games and interactive creativity. //rant! – Confused Nov 17 '17 at 11:15
  • On strobing, there's a lot of work that the movie studios have done on field of view, contrast of objects, proximity to camera and shutter speeds to figure out how to avoid a strobing effect in the eyes of viewers when they pan. Perhaps one of the biggest take-aways from this stuff was that the best thing to do is avoid any possible harmonics in motion in front of the lens/camera. And those balls are showing nearly perfect harmonics. Each frame they're the same distance apart, exaggeration the brain's opportunities to trick itself into persisting imagery while it tries deal with this. – Confused Nov 17 '17 at 11:24
  • Normally, pinballs have a lot of this sorted because they accelerate and decelerate in such an extreme way that they're never at the same speed for long enough to create consistent strobing-like harmonics. So if you can make the balls change rate as though under the influence of extreme gravity/friction this might help, too. – Confused Nov 17 '17 at 11:26
  • on ios it must be possible because other simple games achieve this -- like the breakout tutorial. – Crashalot Nov 17 '17 at 19:53
  • can you show me video of how fast that's running? What size are the objects, how much contrast is in the scene? – Confused Nov 17 '17 at 20:03
  • the breakout tutorial? if yes, the ball has a radius of 8 same as the ball here. – Crashalot Nov 17 '17 at 20:04
  • are you talking about this: https://www.raywenderlich.com/123393/how-to-create-a-breakout-game-with-sprite-kit-and-swift – Confused Nov 17 '17 at 20:06
  • yup that's the one! – Crashalot Nov 17 '17 at 20:07
  • Ok, the differences you're seeing are: Much lower contrast in Ray's version, larger ball relative to screen (much larger), the use of similar levels of saturation in the background and the ball, and the MUCH slower rate of movement (relative to size of ball). All, combined, massively reduce the persistence of vision effect. – Confused Nov 17 '17 at 20:08
  • But the biggest single difference is light background darker object versus bright object and dark background. You've seen this yourself, when you read bright text on a dark background on those sites that get the contrast wrong, and your eyes feel weird when you switch to another normal screen. – Confused Nov 17 '17 at 20:09
  • If you really want to "solve" this "problem" (it's an effect our eyes and brain create in response to missing and dissonant or otherwise unusual visual harmonics) you'll need to consider an engine that can do blurring. SceneKit can, to some extent. You could fake it with a lot of code in SpriteKit and a good deal of experimenting with shaders, too. – Confused Nov 17 '17 at 20:12
  • hmmm ok lemme try decreasing the contrast to see if the effect goes away, but re the ball and screen size, it doesn't happen on the phone? – Crashalot Nov 17 '17 at 20:13
  • But the easiest solution: Ball and background have similar saturation. Use a slightly lighter background than the ball. Don't make the contrast too great. Make the ball bigger. Make it move slower relative to its size, and add in accelerations that let the player subconsciously anticipate where/what is going on next. – Confused Nov 17 '17 at 20:14
  • on Size: it's size relative to the distance travelled between frames and the impression it takes up on the viewing area, and a function of the amount of contrast, too. All factors combine. Lerping the camera is the best way to solve this. Do you have somewhere I can send you a video of what I'm doing, so you can see what I mean by assistive lerping, versus sometimes shocking the player by forcing them to track something fast across physical screen space. – Confused Nov 17 '17 at 20:16