ios – How to get a ordinary Mixamo character animation working in SceneKit?

First, only use your character in the T position. Download the file as Collada (DAE) with the Skin. Do NOT include any animations with this file. No further changes to this file are required.

So, for any animation effect you want to implement like walking, running, dancing or whatever – do it like this:

Test/apply your desired animation in Mixamo on the figure, adjust the settings as you like and download it. Here it is very important to download as Collada (DAE) and select WITHOUT Skin!!! Leave Framerate and keyframe reduction as default.

This will give you a single DAE file for each animation you want to implement. This DAE contains no mesh data and no rig. It only contains the deformations of the model it belongs to (that’s why you choose to download it without Skin).

After that, you need to perform two additional actions on all DAE files that contain animations.

First, print the XML structure for each DAE that contains an animation. You can do this i.Ex. using the XML tools in Notepad++, or you open a terminal on your Mac and use this command:

xmllint —-format my_anim_orig.dae > my_anim.dae

Then install this tool here on your Mac. (https://drive.google.com/file/d/0B1_uvI21ZYGUaGdJckdwaTRZUEk/edit?usp=sharing)
Conversion tool

Convert all your DAE animations with this converter: (But DO NOT convert your T-Pose model using this tool!!!)
Collada converter

No, we are ready to configure the animation:

you should organize the DAEs within art.scnassets folder
DAE files

Let’s configure this:

I usually organize this within a struct called characters. But any other implementation will do

add this:

struct Characters {
    
    // MARK: Characters
    var bodyWarrior                         : SCNNode!
    
    private let objectMaterialWarrior      : SCNMaterial = {
        let material = SCNMaterial()
        material.name                       = "warrior"
        material.diffuse.contents           = UIImage.init(named: "art.scnassets/warrior/textures/warrior_diffuse.png")
        material.normal.contents            = UIImage.init(named: "art.scnassets/warrior/textures/warrior_normal.png")
        material.metalness.contents         = UIImage.init(named: "art.scnassets/warrior/textures/warrior_metalness.png")
        material.roughness.contents         = UIImage.init(named: "art.scnassets/warrior/textures/warrior_roughness.png")
        material.ambientOcclusion.contents  = UIImage.init(named: "art.scnassets/warrior/textures/warrior_AO.png")
        material.lightingModel              = .physicallyBased
        material.isDoubleSided              = false
        return material
    }()
    
    // MARK: MAIN Init Function
    init() {
        
        // Init Warrior
        bodyWarrior = SCNNode(named: "art.scnassets/warrior/warrior.dae")
        bodyWarrior.childNodes[1].geometry?.firstMaterial = objectMaterialWarrior // character body material
        
        print("Characters Init Completed.")
        
    }
    
}

Then you can init struct i.Ex. in viewDidLoad var character = Character()

Be careful to use the correct childNodes!
children's knots

in this case childNodes[1] is the visible mesh and childNodes[0] then becomes the animation node.

you can also implement this SceneKit extension to your code, it is very useful to import models. (note, this will organize the model nodes as Childs from a new node!)

extension SCNNode {
    convenience init(named name: String) {
        self.init()
        guard let scene = SCNScene(named: name) else {return}
        for childNode in scene.rootNode.childNodes {addChildNode(childNode)}
    }
}

also add this extension below. You will need it for the animation player later.

extension SCNAnimationPlayer {
    class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
        let scene = SCNScene( named: sceneName )!
        // find top level animation
        var animationPlayer: SCNAnimationPlayer! = nil
        scene.rootNode.enumerateChildNodes { (child, stop) in
            if !child.animationKeys.isEmpty {
                animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
                stop.pointee = true
            }
        }
        return animationPlayer
    }
}

Handle character setup and animation like this: (here is a simplified version of my class)

class Warrior {
    
    // Main Nodes
    var node                 = SCNNode()
    private var animNode     : SCNNode!
    
    // Control Variables
    var isIdle               : Bool = true
    
    // For Initial Warrior Position and Scale
    private var position            = SCNMatrix4Mult(SCNMatrix4MakeRotation(0,0,0,0), SCNMatrix4MakeTranslation(0,0,0))
    private var scale               = SCNMatrix4MakeScale(0.03, 0.03, 0.03) // default size ca 6m height
    
    // MARK: ANIMATIONS
    private let aniKEY_NeutralIdle       : String = "NeutralIdle-1"       ; private let aniMAT_NeutralIdle       : String = "art.scnassets/warrior/NeutralIdle.dae"
    private let aniKEY_DwarfIdle         : String = "DwarfIdle-1"         ; private let aniMAT_DwarfIdle         : String = "art.scnassets/warrior/DwarfIdle.dae"
    private let aniKEY_LookAroundIdle    : String = "LookAroundIdle-1"    ; private let aniMAT_LookAroundIdle    : String = "art.scnassets/warrior/LookAround.dae"
    private let aniKEY_Stomp             : String = "Stomp-1"             ; private let aniMAT_Stomp             : String = "art.scnassets/warrior/Stomp.dae"
    private let aniKEY_ThrowObject       : String = "ThrowObject-1"       ; private let aniMAT_ThrowObject       : String = "art.scnassets/warrior/ThrowObject.dae"
    private let aniKEY_FlyingBackDeath   : String = "FlyingBackDeath-1"   ; private let aniMAT_FlyingBackDeath   : String = "art.scnassets/warrior/FlyingBackDeath.dae"
    
    // MARK: MAIN CLASS INIT
    init(index: Int, scaleFactor: Float = 0.03) {
        
        scale = SCNMatrix4MakeScale(scaleFactor, scaleFactor, scaleFactor)
        
        // Config Node
        node.index = index
        node.name = "warrior"
        node.addChildNode(GameViewController.characters.bodyWarrior.clone()) // childNodes[0] of node. this holds all subnodes for the character including animation skeletton
        node.childNodes[0].transform = SCNMatrix4Mult(position, scale)
        
        // Set permanent animation Node
        animNode = node.childNodes[0].childNodes[0]
        
        // Add to Scene
        gameScene.rootNode.addChildNode(node) // add the warrior to scene
        
        print("Warrior initialized with index: \(String(describing: node.index))")
        
    }
    
    
    // Cleanup & Deinit
    func remove() {
        print("Warrior deinitializing")
        self.animNode.removeAllAnimations()
        self.node.removeAllActions()
        self.node.removeFromParentNode()
    }
    deinit { remove() }
    
    // Set Warrior Position
    func setPosition(position: SCNVector3) { self.node.position = position }
    
    // Normal Idle
    enum IdleType: Int {
        case NeutralIdle
        case DwarfIdle // observe Fingers
        case LookAroundIdle
    }
    
    // Normal Idles
    func idle(type: IdleType) {
        
        isIdle = true // also sets all walking and running variabled to false
        
        var animationName : String = ""
        var key           : String = ""
        
        switch type {
        case .NeutralIdle:       animationName = aniMAT_NeutralIdle        ; key = aniKEY_NeutralIdle      // ; print("NeutralIdle   ")
        case .DwarfIdle:         animationName = aniMAT_DwarfIdle          ; key = aniKEY_DwarfIdle        // ; print("DwarfIdle     ")
        case .LookAroundIdle:    animationName = aniMAT_LookAroundIdle     ; key = aniKEY_LookAroundIdle   // ; print("LookAroundIdle")
        }
        
        makeAnimation(animationName, key, self.animNode, backwards: false, once: false, speed: 1.0, blendIn: 0.5, blendOut: 0.5)
        
    }
    
    func idleRandom() {
        switch Int.random(in: 1...3) {
        case 1: self.idle(type: .NeutralIdle)
        case 2: self.idle(type: .DwarfIdle)
        case 3: self.idle(type: .LookAroundIdle)
        default: break
        }
    }
    
    // MARK: Private Functions
    // Common Animation Function
    private func makeAnimation(_ fileName           : String,
                               _ key                : String,
                               _ node               : SCNNode,
                               backwards            : Bool = false,
                               once                 : Bool = true,
                               speed                : CGFloat = 1.0,
                               blendIn              : TimeInterval = 0.2,
                               blendOut             : TimeInterval = 0.2,
                               removedWhenComplete  : Bool = true,
                               fillForward          : Bool = false
                              )
    
    {
        
        let anim   = SCNAnimationPlayer.loadAnimation(fromSceneNamed: fileName)
        
        if once { anim.animation.repeatCount = 0 }
        anim.animation.autoreverses = false
        anim.animation.blendInDuration  = blendIn
        anim.animation.blendOutDuration = blendOut
        anim.speed = speed; if backwards {anim.speed = -anim.speed}
        anim.stop()
        print("duration: \(anim.animation.duration)")
        
        anim.animation.isRemovedOnCompletion = removedWhenComplete
        anim.animation.fillsForward          = fillForward
        anim.animation.fillsBackward         = false
        
        // Attach Animation
        node.addAnimationPlayer(anim, forKey: key)
        node.animationPlayer(forKey: key)?.play()
        
    }
    
}

you can then initialize the class object after you initialize the character structure.

the rest you’ll figure out, get back to me if you have any questions or need a full sample app 🙂

William

Leave a Reply