diff --git a/Keychy/Keychy/Core/KeyringBundle/Scene/MultiKeyringScene.swift b/Keychy/Keychy/Core/KeyringBundle/Scene/MultiKeyringScene.swift index 9615132d1..f52931806 100644 --- a/Keychy/Keychy/Core/KeyringBundle/Scene/MultiKeyringScene.swift +++ b/Keychy/Keychy/Core/KeyringBundle/Scene/MultiKeyringScene.swift @@ -86,6 +86,9 @@ class MultiKeyringScene: SKScene { var carabinerBackImageURL: String? // 카라비너 뒷면 이미지 (hamburger 타입) var carabinerFrontImageURL: String? // 카라비너 앞면 이미지 (hamburger 타입) + // MARK: - 영상 생성용 최적화 플래그 + var disableShadows: Bool = false // 그림자 비활성화 (영상 생성 시 성능 최적화) + // MARK: - 카라비너 크기 및 위치 정보 var carabinerId: String = "" // 카라비너 ID (bundleKeyringScale용) var carabinerX: CGFloat = 0 // 카라비너 중심 X 좌표 @@ -243,6 +246,8 @@ class MultiKeyringScene: SKScene { /// - offsetY: Y축 오프셋 (기본값 -8) /// - blurRadius: Gaussian Blur 강도 (기본값 5.0) private func addShadowToNode(_ node: SKSpriteNode, offsetX: CGFloat = 8, offsetY: CGFloat = -8, blurRadius: CGFloat = 5.0) { + // 영상 생성 시 그림자 비활성화 (성능 최적화) + guard !disableShadows else { return } // 원본 노드를 복제해서 그림자로 사용 guard let shadowNode = node.copy() as? SKSpriteNode else { return } diff --git a/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Particle.swift b/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Particle.swift index 8563fab52..06d8c139e 100644 --- a/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Particle.swift +++ b/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Particle.swift @@ -14,13 +14,12 @@ import Lottie extension BundleVideoGenerator { /// 파티클 재생 정보 - /// 현재 재생 중인 파티클을 렌더링하기 위해 필요한 모든 정보를 담고 있음 + /// 시작 시점에 모든 프레임을 프리렌더링하여 재생 중에는 캐시된 텍스처만 사용 struct ParticlePlaybackInfo { - let displaySprite: SKSpriteNode // 화면에 표시되는 스프라이트 - let startedAtFrame: Int // 파티클 시작 프레임 - let particleId: String // 파티클 ID - let lottieRenderer: LottieAnimationView // Lottie → 이미지 변환 렌더러 - let animationData: LottieAnimation // Lottie 메타데이터 (총 프레임 수 등) + let displaySprite: SKSpriteNode // 화면에 표시되는 스프라이트 + let startedAtFrame: Int // 파티클 시작 프레임 + let particleId: String // 파티클 ID + let preRenderedTextures: [SKTexture] // 프리렌더링된 텍스처 배열 } /// 파티클 업데이트 @@ -43,36 +42,69 @@ extension BundleVideoGenerator { particleIndicesToRemove.forEach { playingParticles.removeValue(forKey: $0) } } - /// 파티클 시작 + /// 파티클 시작 (모든 프레임을 프리렌더링) private func startParticle(for keyringIndex: Int, particleId: String, at frameIndex: Int, scene: MultiKeyringScene) { guard let animation = findParticleAnimation(particleId: particleId) else { return } + // Lottie 뷰 설정 let config = LottieConfiguration(renderingEngine: .mainThread) let lottieView = LottieAnimationView(animation: animation, configuration: config) lottieView.frame = CGRect(origin: .zero, size: CGSize(width: scene.size.width, height: scene.size.height)) lottieView.contentMode = .scaleAspectFit lottieView.backgroundBehavior = .pauseAndRestore + // 모든 프레임을 프리렌더링 + let textures = preRenderAllFrames(lottieView: lottieView, animation: animation) + + // 스프라이트 생성 let sprite = SKSpriteNode() sprite.size = CGSize(width: scene.size.width, height: scene.size.height) sprite.position = CGPoint(x: scene.size.width / 2, y: scene.size.height / 2) sprite.zPosition = 100 sprite.alpha = 1.0 + // 첫 프레임 텍스처 설정 + if let firstTexture = textures.first { + sprite.texture = firstTexture + } + scene.addChild(sprite) playingParticles[keyringIndex] = ParticlePlaybackInfo( displaySprite: sprite, startedAtFrame: frameIndex, particleId: particleId, - lottieRenderer: lottieView, - animationData: animation + preRenderedTextures: textures ) } - /// 파티클 렌더링 + /// Lottie 애니메이션의 모든 프레임을 SKTexture로 프리렌더링 + private func preRenderAllFrames(lottieView: LottieAnimationView, animation: LottieAnimation) -> [SKTexture] { + let totalFrames = Int(animation.endFrame - animation.startFrame) + var textures: [SKTexture] = [] + textures.reserveCapacity(totalFrames) + + // UIGraphicsImageRenderer를 한 번만 생성 (재사용) + let imageRenderer = UIGraphicsImageRenderer(bounds: lottieView.bounds) + + for frameOffset in 0.. Bool { guard let particleInfo = playingParticles[keyringIndex] else { return false @@ -80,23 +112,15 @@ extension BundleVideoGenerator { let sprite = particleInfo.displaySprite let offset = frameIndex - particleInfo.startedAtFrame - let targetFrame = particleInfo.animationData.startFrame + CGFloat(offset) - if targetFrame >= particleInfo.animationData.endFrame { + // 애니메이션 종료 체크 + if offset >= particleInfo.preRenderedTextures.count { sprite.removeFromParent() return true } - particleInfo.lottieRenderer.currentFrame = AnimationFrameTime(targetFrame) - particleInfo.lottieRenderer.setNeedsDisplay() - particleInfo.lottieRenderer.layer.displayIfNeeded() - - let imageRenderer = UIGraphicsImageRenderer(bounds: particleInfo.lottieRenderer.bounds) - let image = imageRenderer.image { context in - particleInfo.lottieRenderer.layer.render(in: context.cgContext) - } - - sprite.texture = SKTexture(image: image) + // 프리렌더링된 텍스처 사용 (매우 빠름) + sprite.texture = particleInfo.preRenderedTextures[offset] return false } diff --git a/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Setup.swift b/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Setup.swift index a65193279..1b4422d90 100644 --- a/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Setup.swift +++ b/Keychy/Keychy/Core/Video/Bundle/BundleVideoGenerator+Setup.swift @@ -62,6 +62,7 @@ extension BundleVideoGenerator { scene.currentCarabinerType = carabinerType scene.scaleMode = .aspectFill scene.size = CGSize(width: sceneWidth, height: sceneHeight) + scene.disableShadows = true // 영상 생성 시 그림자 비활성화 (성능 최적화) if let bgImage = backgroundImage { let backgroundNode = SKSpriteNode(texture: SKTexture(image: bgImage))