Search code examples
swiftios11scenekitaugmented-realityarkit

ARKit – Loading GIF image in SCNNode using CALayer not rendering properly


I am working on an AR application in which I am rendering a 3D model.

Upon tapping a child node I am displaying a GIF image by creating animation using CALayer and loading it in diffuse.contents by following this thread Set contents of CALayer to animated GIF?

let animation : CAKeyframeAnimation = createGIFAnimation(url: gifImageURL!)!
let layer = CALayer()
layer.bounds = CGRect(x: 0, y: 0, width:600, height:200)
layer.add(animation, forKey: "contents")

let newMaterial = SCNMaterial()
newMaterial.isDoubleSided = true
newMaterial.diffuse.contents = layer
let plane = SCNPlane(width: 5, height: 5)
plane.materials = [newMaterial]
let node = SCNNode(geometry: plane)
self.sceneView.scene.rootNode.addChildNode(node)

I am able to render the gif image in SCNNode but it is displaying only quarter of it (right bottom most portion of GIF is only visible). I tried below steps to rectify this but still in vain.

  1. Changing Plane's width/height
  2. Changing the bounds of CALayer
  3. Tried with different sizes of gif image

Anyone please help me how to render a full gif image in SCNPlane using CALayer.


Solution

  • As a workaround I followed the below procedure to load the GIF properly:

    let plane = SCNPlane(width: 2, height: 2)
    
    let bundleURL = Bundle.main.url(forResource: "engine", withExtension: "gif")
    let animation : CAKeyframeAnimation = createGIFAnimation(url: bundleURL!)!
    let layer = CALayer()
    layer.bounds = CGRect(x: 0, y: 0, width: 900, height: 900)
    
    layer.add(animation, forKey: "contents")
    let tempView = UIView.init(frame: CGRect(x: 0, y: 0, width: 900, height: 900))
    tempView.layer.bounds = CGRect(x: -450, y: -450, width: tempView.frame.size.width, height: tempView.frame.size.height)
    tempView.layer.addSublayer(layer)
    
    let newMaterial = SCNMaterial()
    newMaterial.isDoubleSided = true
    newMaterial.diffuse.contents = tempView.layer
    plane.materials = [newMaterial]
    let node = SCNNode(geometry: plane)
    node.name = "engineGif"
    let gifImagePosition = SCNVector3Make((self.virtualObjectInteraction.selectedObject?.childNodes[0].position.x)! + 2, 
                                          (self.virtualObjectInteraction.selectedObject?.childNodes[0].position.y)! + 4, 
                                          (self.virtualObjectInteraction.selectedObject?.childNodes[0].position.z)! - 2)
    node.position = gifImagePosition
    self.virtualObjectInteraction.selectedObject?.childNodes[0].addChildNode(node)
    

    Where createGIFAnimation creates the required animation,

    func createGIFAnimation(url:URL) -> CAKeyframeAnimation? {
    
        guard let src = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
        let frameCount = CGImageSourceGetCount(src)
    
        // Total loop time
        var time : Float = 0
    
        // Arrays
        var framesArray = [AnyObject]()
        var tempTimesArray = [NSNumber]()
    
        // Loop
        for i in 0..<frameCount {
    
            // Frame default duration
            var frameDuration : Float = 0.1;
    
            let cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(src, i, nil)
            guard let framePrpoerties = cfFrameProperties as? [String:AnyObject] else {return nil}
            guard let gifProperties = framePrpoerties[kCGImagePropertyGIFDictionary as String] as? [String:AnyObject]
                else { return nil }
    
            // Use kCGImagePropertyGIFUnclampedDelayTime or kCGImagePropertyGIFDelayTime
            if let delayTimeUnclampedProp = gifProperties[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber {
                frameDuration = delayTimeUnclampedProp.floatValue
            } else {
                if let delayTimeProp = gifProperties[kCGImagePropertyGIFDelayTime as String] as? NSNumber {
                    frameDuration = delayTimeProp.floatValue
                }
            }
    
            // Make sure its not too small
            if frameDuration < 0.011 {
                frameDuration = 0.100;
            }
    
            // Add frame to array of frames
            if let frame = CGImageSourceCreateImageAtIndex(src, i, nil) {
                tempTimesArray.append(NSNumber(value: frameDuration))
                framesArray.append(frame)
            }
    
            // Compile total loop time
            time = time + frameDuration
        }
    
        var timesArray = [NSNumber]()
        var base : Float = 0
        for duration in tempTimesArray {
            timesArray.append(NSNumber(value: base))
            base += ( duration.floatValue / time )
        }
    
        // From documentation of 'CAKeyframeAnimation':
        // the first value in the array must be 0.0 and the last value must be 1.0.
        // The array should have one more entry than appears in the values array.
        // For example, if there are two values, there should be three key times.
        timesArray.append(NSNumber(value: 1.0))
    
        // Create animation
        let animation = CAKeyframeAnimation(keyPath: "contents")
    
        animation.beginTime = AVCoreAnimationBeginTimeAtZero
        animation.duration = CFTimeInterval(time)
        animation.repeatCount = Float.greatestFiniteMagnitude;
        animation.isRemovedOnCompletion = false
        animation.fillMode = kCAFillModeForwards
        animation.values = framesArray
        animation.keyTimes = timesArray
        //animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
        animation.calculationMode = kCAAnimationDiscrete
    
        return animation;
    }