Реалізація просмотра 360°-панорам у мобільному додатку
360°-панорами на відміну від статичних 360°-фото — це либо відео в equirectangular-форматі, либо інтерактивні сцени з точками інтересу (hotspots), переходами між локаціями й аудіогідом. Технічні вимоги значно вищі, ніж для одиничного фото: потрібна декодування відео 4K/8K у реальному часі або управління набором висок orозрішених тайлів з плавним переходом.
Відео-панорами: де все ломається
360°-відео в equirectangular-форматі на iOS декодується через AVPlayer з AVPlayerLayer, але AVPlayerLayer рендерит тільки в 2D. Для сферичної проекції потрібен AVPlayerItemVideoOutput + Metal або SceneKit.
Ключова проблема: AVPlayerItemVideoOutput.copyPixelBuffer(forItemTime:itemTimeForDisplay:) блокує вивих потік на час декодування кадра. На iPhone 12 декодування 4K H.265 займає 8–15ms — це половина бюджету кадра при 60fps. Виклик на CADisplayLink-callback у main thread дає видимі просадки.
Правильно: CADisplayLink тільки запускає Metal render pass, копіювання CVPixelBuffer — у окремій DispatchQueue(qos: .userInteractive), результат передається в Metal через CVMetalTextureCacheCreateTextureFromImage.
let displayLink = CADisplayLink(target: self, selector: #selector(renderFrame))
displayLink.preferredFrameRateRange = CAFrameRateRange(minimum: 60, maximum: 120, preferred: 120)
@objc func renderFrame() {
videoDecodeQueue.async { [weak self] in
guard let pixelBuffer = self?.videoOutput.copyPixelBuffer(
forItemTime: self!.playerItem.currentTime(),
itemTimeForDisplay: nil
) else { return }
self?.metalRenderer.render(pixelBuffer: pixelBuffer)
}
}
Тайлові панорами (віртуальні тури)
Для висок orозрішених статичних панорам (готелі, нерухомість, музеї) використовують не єдине зображення, а тайлів по аналогії з картами: кілька рівнів деталізації, розбитих на сітку. При приближенні (зменшенні FOV) підгружаються тайлі більш високого розширення.
Стандарт — Krpano tile format або Marzipano. Для мобільного рендера використовуємо власний Metal/OpenGL ES пайплайн або PanoramaGL (Android). Тайлі загружаються через URLSession з приоритетами: спочатку видимий квадрант сфери, потім сусідні 4 тайлі (prefetch).
LRU-кеш для тайлів — обов'язково. Без нього при навігації по віртуальному туру (10+ локацій × 6 граней куба × 4 рівні деталізації) пам'ять виростає до 800 МБ за 15 хвилин.
Hotspots й інтерактивність
Hotspot — точка у 3D-просторі сфери, яка при рендерингу перетворюється на 2D-елемент на екрані (іконка, підказка, кнопка переходу). Конвертація зі сферичних координат (yaw/pitch) в screen coordinates:
func sphericalToScreen(yaw: Float, pitch: Float,
cameraYaw: Float, cameraPitch: Float,
fov: Float, screenSize: CGSize) -> CGPoint? {
// Матричне перетворення: сферичні → декартові → проекція камери → NDC → screen
let direction = SIMD3<Float>(
cos(pitch) * sin(yaw),
sin(pitch),
cos(pitch) * cos(yaw)
)
// ... view matrix × projection matrix → clip space → viewport
}
Якщо dot(direction, cameraForward) < 0 — hotspot за камерою, не рендеримо.
Анімація hotspot-появлення при попаданні в FOV — fade-in через CABasicAnimation, не через SwiftUI-анімацію (SwiftUI overlay поверх Metal SCNView дає 2–3ms layout pass на кожний кадр).
Гіроскоп й компас
CMDeviceMotion через CMMotionManager дає quaternion орієнтації пристрою з частотою до 100 Hz. Перетворюємо в Euler-кути для камери сцени, застосовуємо фільтр Калмана для сгладжування джиттера. Без фільтра гіроскопний дрейф на старих пристроях (~0.3° на секунду) за 5 хвилин уводить «север» на 15°.
Північне направління при старті — з CLLocationManager.heading, привязуємо нульовий yaw до магнітного/істинного півночі.
Терміни
Відео-панорама 4K з Metal-рендером (iOS): 2–3 тижні. Віртуальний тур з тайлами, hotspots, переходами між локаціями, iOS + Android: 6–10 тижнів залежно від кількості інтерактивних елементів. Вартість розраховується індивідуально.







