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

如何通过SCNAction/SCNTransaction实现SceneKit选中节点单独闪烁

实现单个共享材质节点的闪烁效果(SCNAction/SCNTransaction版)

嘿,这个需求我太熟了!共享材质的情况下单独让某个节点闪烁,确实不能直接改原材质——不然所有用这个材质的节点都会跟着闪。之前在Update里硬写循环的方式确实冗余又难维护,下面给你两种干净的实现方案,完全不用碰Update(),代码简洁还易扩展。

核心前提:先给目标节点做材质隔离

因为SCNMaterial是引用类型,多个节点共享同一个实例时,修改材质属性会影响所有关联节点。所以第一步必须给要闪烁的节点创建独立的材质副本,确保动画只作用于它:

// 假设targetNode是你选中的要闪烁的节点
guard let originalMaterial = targetNode.geometry?.firstMaterial else { return }
// 复制原材质,得到独立实例
let flashingMaterial = originalMaterial.copy() as! SCNMaterial
// 替换目标节点的材质
targetNode.geometry?.firstMaterial = flashingMaterial

方案一:用SCNAction实现循环闪烁(推荐)

SCNAction是SceneKit专门用来处理节点动画的工具,自带循环、序列等逻辑,完全不用手动维护状态。这里我们实现「原颜色 ↔ 闪烁颜色」的平滑过渡循环:

纯色材质的闪烁

如果你的材质用的是纯色,可以直接动画diffuse属性:

// 定义闪烁的目标颜色
let flashColor = UIColor.red
// 获取材质当前的基础颜色(默认白色兜底)
let originalColor = flashingMaterial.diffuse.contents as? UIColor ?? .white

// 创建「从原颜色过渡到闪烁色」的动作
let fadeToFlash = SCNAction.customAction(duration: 0.5) { node, elapsedTime in
    guard let material = node.geometry?.firstMaterial else { return }
    let progress = elapsedTime / 0.5 // 计算动画进度(0~1)
    // 颜色插值,实现平滑过渡
    material.diffuse.contents = originalColor.blended(withFraction: progress, of: flashColor)
}

// 创建「从闪烁色过渡回原颜色」的动作
let fadeBackToOriginal = SCNAction.customAction(duration: 0.5) { node, elapsedTime in
    guard let material = node.geometry?.firstMaterial else { return }
    let progress = elapsedTime / 0.5
    material.diffuse.contents = flashColor.blended(withFraction: progress, of: originalColor)
}

// 组合成循环动作:先闪过去,再闪回来,无限重复
let flashLoop = SCNAction.repeatForever(SCNAction.sequence([fadeToFlash, fadeBackToOriginal]))

// 给目标节点添加动画(指定key方便后续停止)
targetNode.runAction(flashLoop, forKey: "nodeFlash")

纹理材质的闪烁(更高效)

如果你的材质用的是纹理,没必要修改纹理本身——可以动画emission(自发光)属性,让节点在「正常状态 ↔ 高亮自发光」之间切换,视觉效果更好,性能也更高:

let originalEmission = flashingMaterial.emission.contents as? UIColor ?? .clear
let flashEmission = UIColor.red.withAlphaComponent(0.8)

let fadeToGlow = SCNAction.customAction(duration: 0.5) { node, elapsedTime in
    guard let material = node.geometry?.firstMaterial else { return }
    let progress = elapsedTime / 0.5
    material.emission.contents = originalEmission.blended(withFraction: progress, of: flashEmission)
}

let fadeBackToNormal = SCNAction.customAction(duration: 0.5) { node, elapsedTime in
    guard let material = node.geometry?.firstMaterial else { return }
    let progress = elapsedTime / 0.5
    material.emission.contents = flashEmission.blended(withFraction: progress, of: originalEmission)
}

let glowLoop = SCNAction.repeatForever(SCNAction.sequence([fadeToGlow, fadeBackToNormal]))
targetNode.runAction(glowLoop, forKey: "nodeGlow")

停止闪烁

需要停止的时候,直接移除对应的动作即可:

targetNode.removeAction(forKey: "nodeFlash") // 或者"nodeGlow"

方案二:用SCNTransaction实现精细控制

SCNTransaction适合需要更精细动画控制的场景(比如自定义时序曲线),通过回调递归实现循环:

// 先定义一个标志位,方便随时停止闪烁
var isFlashing = true

func startFlashing(node: SCNNode, originalColor: UIColor, flashColor: UIColor) {
    guard isFlashing, let material = node.geometry?.firstMaterial else { return }
    
    SCNTransaction.begin()
    SCNTransaction.animationDuration = 0.5
    // 自定义动画曲线(可选,比如先快后慢)
    SCNTransaction.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
    
    // 动画完成后递归调用,实现循环
    SCNTransaction.completionBlock = {
        let nextColor = material.diffuse.contents as? UIColor == originalColor ? flashColor : originalColor
        self.startFlashing(node: node, originalColor: originalColor, flashColor: nextColor)
    }
    
    // 切换材质颜色
    material.diffuse.contents = material.diffuse.contents as? UIColor == originalColor ? flashColor : originalColor
    
    SCNTransaction.commit()
}

// 调用方式
guard let originalMaterial = targetNode.geometry?.firstMaterial else { return }
let flashingMaterial = originalMaterial.copy() as! SCNMaterial
targetNode.geometry?.firstMaterial = flashingMaterial
let originalColor = flashingMaterial.diffuse.contents as? UIColor ?? .white
isFlashing = true
startFlashing(node: targetNode, originalColor: originalColor, flashColor: .red)

// 停止闪烁时只需设置标志位
isFlashing = false

关键注意事项

  • 永远不要直接修改共享材质的属性:一定要先复制出独立实例,否则所有共享节点都会同步变化。
  • 优先用SCNAction:对于循环类动画,SCNAction的封装更完善,不用手动维护状态和递归逻辑,代码更简洁。
  • 纹理材质优先动画emission:避免修改纹理带来的性能开销,同时视觉效果更自然。

内容的提问来源于stack exchange,提问作者oliver

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 10:23:03