iOS 18物理设备上SceneKit骨骼动画过渡无平滑效果问题
iOS 18 物理设备上 SceneKit 骨骼动画过渡失效问题
我开发3D移动游戏,使用Maya导出的.dae格式角色模型及CAAnimation骨骼动画,通过设置fadeInDuration和fadeOutDuration实现动画间随机连续的平滑切换。该功能在iOS 17.5上运行正常,但升级至iOS 18后,物理设备上的动画过渡变得生硬,这两个参数似乎被忽略,而iOS 18模拟器仍能保持平滑过渡。
实现代码
import UIKit import QuartzCore import SceneKit class TestAnimationController: UIViewController { var bodyNode: SCNNode? override func viewDidLoad() { super.viewDidLoad() let scnView = SCNView(frame: self.view.bounds) scnView.backgroundColor = .black scnView.autoresizingMask = [.flexibleWidth, .flexibleHeight] let scene = SCNScene(named: "art.scnassets/scene/Base_room/ROOM5.scn")! bodyNode = collada2SCNNode(filepath: "art.scnassets/female/girl_body_races.dae")! bodyNode?.renderingOrder = 10 scene.rootNode.addChildNode(bodyNode!) playIdleAnimation() scnView.scene = scene self.view.addSubview(scnView) } func collada2SCNNode(filepath: String) -> SCNNode? { if let scene = SCNScene(named: filepath) { let node = scene.rootNode.childNodes[0] return node } else { return nil } } func playIdleAnimation() { let array = [ "art.scnassets/female/animations/idle/girl_idle_4.dae", "art.scnassets/female/animations/idle/girl_idle1.dae", "art.scnassets/female/animations/idle/girl_idle2.dae", "art.scnassets/female/animations/idle/Girl_idle3.dae", ] let animation = CAAnimation.animationWithSceneNamed(array.randomElement() ?? "")! self.setAnimationAdd( fadeInDuration: 1.0, fadeOutDuration: 1.0, keyTime: 0.99, animation, isLooped: false ) { [weak self] in guard let self = self else { return } self.playBoringAnimations() } } func playBoringAnimations() { let array = [ "art.scnassets/female/animations/boring/girl_boring1.dae", "art.scnassets/female/animations/boring/girl_boring2.dae", "art.scnassets/female/animations/boring/girl_boring3.dae", "art.scnassets/female/animations/boring/girl_boring4.dae", "art.scnassets/female/animations/boring/girl_boring5.dae", "art.scnassets/female/animations/boring/girl_boring6.dae", "art.scnassets/female/animations/boring/girl_boring8.dae" ] let animation = CAAnimation.animationWithSceneNamed(array.randomElement() ?? "")! self.setAnimationAdd( fadeInDuration: 1.0, fadeOutDuration: 1.0, keyTime: 0.99, animation, isLooped: false ) { [weak self] in guard let self = self else { return } self.playIdleAnimation() } } func setAnimationAdd(fadeInDuration : CGFloat, fadeOutDuration : CGFloat, keyTime : CGFloat, _ animation: CAAnimation, isLooped: Bool, completion: (() -> Void)?) { animation.fadeInDuration = fadeInDuration animation.fadeOutDuration = fadeOutDuration if !isLooped { animation.repeatCount = 1 } else { animation.repeatCount = Float.greatestFiniteMagnitude } animation.animationEvents = [ SCNAnimationEvent(keyTime: keyTime, block: { _, _, _ in completion?() }) ] bodyNode?.addAnimation(animation, forKey: "avatarAnimation") } }
解决方案尝试
1. 切换到SCNAnimationPlayer管理动画
iOS 18对CAAnimation的动画混合逻辑可能有调整,改用SCNAnimationPlayer能获得更稳定的SceneKit动画控制:
// 替换动画加载方法 func loadAnimationPlayer(from filePath: String) -> SCNAnimationPlayer? { guard let scene = SCNScene(named: filePath) else { return nil } guard let player = scene.rootNode.animationPlayers.values.first else { return nil } let anim = player.animation anim.fadeInDuration = 1.0 anim.fadeOutDuration = 1.0 anim.repeatCount = 1 return player } // 修改播放逻辑 func playIdleAnimation() { let animationPaths = [ "art.scnassets/female/animations/idle/girl_idle_4.dae", "art.scnassets/female/animations/idle/girl_idle1.dae", "art.scnassets/female/animations/idle/girl_idle2.dae", "art.scnassets/female/animations/idle/Girl_idle3.dae", ] guard let player = loadAnimationPlayer(from: animationPaths.randomElement() ?? "") else { return } bodyNode?.addAnimationPlayer(player, forKey: "avatarAnimation") // 基于动画时长计算切换时机 DispatchQueue.main.asyncAfter(deadline: .now() + player.animation.duration) { [weak self] in self?.playBoringAnimations() } } // playBoringAnimations方法做同样修改
2. 调整动画事件触发时机
原代码中keyTime: 0.99可能在iOS18下导致旧动画未完成淡出就被替换,改为基于动画时长动态计算:
func setAnimationAdd(fadeInDuration : CGFloat, fadeOutDuration : CGFloat, _ animation: CAAnimation, isLooped: Bool, completion: (() -> Void)?) { animation.fadeInDuration = fadeInDuration animation.fadeOutDuration = fadeOutDuration animation.repeatCount = isLooped ? Float.greatestFiniteMagnitude : 1 // 计算合理的触发时间,确保淡出完成 let triggerKeyTime = max(0, min(1.0, (animation.duration - fadeOutDuration)/animation.duration)) animation.animationEvents = [ SCNAnimationEvent(keyTime: triggerKeyTime, block: { _, _, _ in completion?() }) ] bodyNode?.addAnimation(animation, forKey: "avatarAnimation") }
3. 手动控制动画混合权重
如果系统fade参数失效,手动通过blendFactor控制动画过渡:
func transitionToNextAnimation(_ nextAnim: CAAnimation) { // 处理当前动画淡出 if let currentAnim = bodyNode?.animation(forKey: "avatarAnimation") as? SCNAnimation { currentAnim.fadeOutDuration = 1.0 currentAnim.isRemovedOnCompletion = true } // 设置新动画淡入 nextAnim.fadeInDuration = 1.0 nextAnim.repeatCount = 1 bodyNode?.addAnimation(nextAnim, forKey: "avatarAnimation") // 延迟切换 DispatchQueue.main.asyncAfter(deadline: .now() + nextAnim.duration) { [weak self] in // 切换到对应动画组 } }
4. 检查iOS 18 API变更
确认是否需要开启usesSceneTimeBase属性,确保动画使用SceneKit的时间轴:
animation.usesSceneTimeBase = true
内容的提问来源于stack exchange,提问作者Максим Шаталов
相关产品推荐
相关产品推荐

