如何通过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
相关产品推荐
相关产品推荐

