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

SpriteKit开发实时视频编辑器:如何将SKScene转换为视频文件

实现SKScene导出视频方案

你的技术方案是可行的,SpriteKit场景可以通过逐帧渲染+AVFoundation写入的方式导出为本地视频文件,以下是完整实现流程:

核心实现步骤

  • 配置AVAssetWriter导出参数:指定导出路径、视频编码格式、分辨率、帧率等参数
  • 创建离线SKView用来渲染场景,不需要添加到当前视图层级
  • 手动逐帧控制SKScene的动画进度,将每帧内容渲染为CMSampleBuffer写入视频
  • 所有帧写入完成后结束写入,拿到导出的视频文件

完整代码示例

首先你的SKScene代码不需要做太大改动,只需要对外暴露动画总时长,方便导出时使用:

class MyScene: SKScene {
    // 动画总时长,对外暴露方便导出时调用
    let animationDuration: TimeInterval = 3.0
    
    override func didMove(to view: SKView) {
        let box2 = SKLabelNode(text: "HELLOO")
        box2.fontSize = 60
        box2.color = .white
        box2.position = CGPoint(x: size.width / 2, y: size.height / 2 - 80)
        addChild(box2)
        
        let actionMove: SKAction = SKAction.move(to: CGPoint(x: 200, y: 100),
                                                 duration: animationDuration)
        box2.run(actionMove)
    }
}

然后是导出工具的核心代码:

import AVFoundation
import SpriteKit
import UIKit

func exportSceneToVideo(sceneSize: CGSize, fps: Int = 30, completion: @escaping (URL?, Error?) -> Void) {
    // 1. 配置导出路径
    let outputPath = NSTemporaryDirectory().appending("exportedVideo.mp4")
    let outputURL = URL(fileURLWithPath: outputPath)
    // 先删除已存在的旧文件
    try? FileManager.default.removeItem(at: outputURL)
    
    // 2. 初始化AVAssetWriter
    guard let writer = try? AVAssetWriter(outputURL: outputURL, fileType: .mp4) else {
        completion(nil, NSError(domain: "ExportError", code: 0, userInfo: [NSLocalizedDescriptionKey: "创建视频写入器失败"]))
        return
    }
    
    // 3. 配置视频写入参数
    let videoSettings: [String: Any] = [
        AVVideoCodecKey: AVVideoCodecType.h264,
        AVVideoWidthKey: sceneSize.width,
        AVVideoHeightKey: sceneSize.height
    ]
    let writerInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings)
    let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: writerInput, sourcePixelBufferAttributes: [
        kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32ARGB,
        kCVPixelBufferWidthKey as String: sceneSize.width,
        kCVPixelBufferHeightKey as String: sceneSize.height
    ] as [String : Any])
    
    writer.add(writerInput)
    writer.startWriting()
    writer.startSession(atSourceTime: .zero)
    
    // 4. 创建离线SKView和场景
    let offscreenView = SKView(frame: CGRect(origin: .zero, size: sceneSize))
    let scene = MyScene(size: sceneSize)
    scene.scaleMode = .aspectFit
    offscreenView.presentScene(scene)
    
    // 5. 逐帧渲染写入
    let totalFrames = Int(scene.animationDuration * Double(fps))
    let frameDuration = CMTimeMake(value: 1, timescale: Int32(fps))
    var currentFrame = 0
    
    let queue = DispatchQueue(label: "videoExportQueue")
    writerInput.requestMediaDataWhenReady(on: queue) {
        while writerInput.isReadyForMoreMediaData, currentFrame < totalFrames {
            let presentationTime = CMTimeMultiply(frameDuration, multiplier: Int32(currentFrame))
            // 手动更新场景时间,控制动画进度
            scene.update(CGFloat(CMTimeGetSeconds(presentationTime)))
            scene.simulatePhysics()
            scene.applyEffects()
            scene.speed = 1.0
            
            // 渲染当前帧为像素缓冲
            var pixelBuffer: CVPixelBuffer?
            CVPixelBufferCreate(kCFAllocatorDefault, Int(sceneSize.width), Int(sceneSize.height), kCVPixelFormatType_32ARGB, nil, &pixelBuffer)
            guard let buffer = pixelBuffer else {
                currentFrame += 1
                continue
            }
            
            CVPixelBufferLockBaseAddress(buffer, [])
            offscreenView.drawHierarchy(in: offscreenView.bounds, afterScreenUpdates: true)
            let context = CGContext(data: CVPixelBufferGetBaseAddress(buffer), width: Int(sceneSize.width), height: Int(sceneSize.height), bitsPerComponent: 8, bytesPerRow: CVPixelBufferGetBytesPerRow(buffer), space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue)
            if let ctx = context {
                UIGraphicsPushContext(ctx)
                offscreenView.layer.render(in: ctx)
                UIGraphicsPopContext()
            }
            CVPixelBufferUnlockBaseAddress(buffer, [])
            
            // 写入像素缓冲
            pixelBufferAdaptor.append(buffer, withPresentationTime: presentationTime)
            CVPixelBufferRelease(buffer)
            
            currentFrame += 1
        }
        
        if currentFrame >= totalFrames {
            writerInput.markAsFinished()
            writer.finishWriting {
                DispatchQueue.main.async {
                    if writer.status == .completed {
                        completion(outputURL, nil)
                    } else {
                        completion(nil, writer.error)
                    }
                }
            }
        }
    }
}

调用方法

直接调用上面的导出方法即可,传入你需要的导出分辨率:

// 可自定义导出分辨率,建议和场景的size比例一致避免拉伸
let exportSize = CGSize(width: 1080, height: 1920)
exportSceneToVideo(sceneSize: exportSize) { outputURL, error in
    if let url = outputURL {
        print("导出成功,路径:\(url)")
        // 可在此处添加保存到相册等后续逻辑
    } else if let err = error {
        print("导出失败:\(err.localizedDescription)")
    }
}

注意事项

  • 如果你的场景包含粒子效果、物理引擎模拟等内容,需要在逐帧更新时额外调用对应的模拟方法,确保导出内容和实际运行效果一致
  • 若需要导出带音频的视频,额外添加音频轨道的AVAssetWriterInput,同步写入音频数据即可
  • 导出过程建议放在子线程执行,避免阻塞主线程UI

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 19:45:03