前往 mixamo.com,选择一个角色,点击动画,选择一个,只需 下载为 .dae。
将文件放在 Mac 桌面上;点击文件信息)。它将完美地动画化角色的移动。
Xcode,拖入文件夹。点击 .dae 文件,点击底部的“播放”图标。它将完美地动画化角色的移动。
现在,将角色添加到现有的 SceneKit 场景中。例如:
let p = Bundle.main.url(forResource: "File Name", withExtension: "dae")!
modelSource = SCNSceneSource(url: p, options: nil)!
let geom = modelSource.entryWithIdentifier("geometry316",
withClass: SCNGeometry.self)! as SCNGeometry
theModel = SCNNode(geometry: geom)
.. your node .. .addChildNode(theModel)
(要获取几何名称,只需查看 .dae 文本example)
您将完美地看到处于 T 姿势的角色
但是似乎不可能在角色上运行动画。
代码看起来像......
theAnime = amySource.entryWithIdentifier("unnamed_animation__0", withClass: CAAnimation.self)!
theModel.addAnimation(theAnime, forKey:"aKey")
无论我尝试什么,它都没有动画。
在添加动画的那一刻,角色会跳转到不同的静态位置,并且不执行任何操作。 (如果您安排“结束”动画
removeAllAnimations()
,它只会返回到 T 形姿势。)
显然dae 文件是完美的,因为它在 Mac 查找器中完美显示了动画,并且在 Xcode 中的 .dae 文件的实际屏幕上完美显示了动画!
简而言之,从上面的 mixamo 图片来看,有人能够让动画在 SceneKit 场景中运行吗?
(PS 不是 ARKit ..场景套件。)
首先,您的角色只需处于 T 字位置。将该文件下载为带有皮肤的 Collada (DAE)。请勿在此文件中包含任何动画。则无需对此文件进行进一步修改。
然后,对于您将实现的任何动画效果,例如行走、跑步、跳舞或其他任何效果 - 这样做:
在 Mixamo 中的角色上测试/应用您想要的动画,根据需要调整设置,然后下载。在这里,下载为 Collada (DAE) 并选择没有皮肤非常重要!将帧速率和关键帧缩减保留为默认值。
这将为您想要实现的每个动画提供一个 DAE 文件。该 DAE 不包含网格数据,也不包含装备。它仅包含其所属模型的变形(这就是您选择下载不带皮肤的原因)。
然后您需要对所有包含动画的 DAE 文件执行两个额外操作。
首先,您需要漂亮地打印包含动画的每个 DAE 的 XML 结构。你可以这样做,即。使用 Notepad++ 中的 XML 工具,或者在 Mac 上打开终端并使用以下命令:
xmllint —-format my_anim_orig.dae > my_anim.dae
然后在您的 Mac 上安装此工具。 (https://drive.google.com/file/d/167Le084XFM5euTm4TRItbhXXPVaPJhZN/view?usp=sharing)
使用此转换器转换所有 DAE 动画: (但请勿使用此工具转换您的 T 形模型!!!)
不,我们已准备好设置动画:
您应该将 DAE 组织在
art.scnassets
文件夹中
让我们配置一下:
我通常将其组织在称为字符的
struct
中。但任何其他实现都可以
添加此:
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.")
}
}
然后你可以初始化结构体 i.Ex。在viewDidLoad中 var 字符 = 字符()
在这种情况下,
childNodes[1]
是可见网格,childNodes[0]
将是动画节点。
您也可以在您的代码中实现此 SceneKit 扩展,它对于导入模型非常有用。 (注意,它会将模型节点组织为新节点的子节点!)
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)}
}
}
还在下面添加该扩展名。稍后您将在动画播放器中需要它。
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
}
}
像这样处理角色设置和动画: (这是我的课程的简化版本)
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()
}
}
您可以在初始化字符结构后初始化类对象。
其余的你会弄清楚的,如果你有疑问或需要一个完整的示例应用程序,请回来找我:)