You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,提问作者Максим Шаталов

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 14:20:55