Реалізація просмотра 360°-фотографій у мобільному додатку
360°-фото — це equirectangular-зображення: сферична проекція, де 360° по горизонталі й 180° по вертикалі упаковані в прямокутник із соотношенням сторін 2:1. На телефоні його потрібно «розгорнути» назад у сферу й дати користувачу дивитися зсередини. Задача не в тому, щоб показати картинку — задача в тому, щоб сфера рендерилась без distortion-артефактів й скролінг не лагав навіть на 50 МПікс JPEG.
Чому стандартний UIImageView або ImageView не працюють
UIImageView відображає плоске зображення. Для сферичної проекції потрібен 3D-рендер: сфера з інвертованими нормалями (щоб дивитися зсередини), equirectangular-текстура на неї, камера в центрі, управління поворотом через гіроскоп або дотик.
SceneKit (SCNSphere) — найшвидший шлях на iOS. Metal — якщо потрібен повний контроль над шейдерами. OpenGL ES на Android через GLSurfaceView або Vulkan для сучасних пристроїв. У більшості проектів використовуємо SceneKit на iOS й Filament/OpenGL ES 3.0 на Android.
Реалізація на iOS з SceneKit
let sceneView = SCNView(frame: view.bounds)
let scene = SCNScene()
// Сфера з інвертованими нормалями
let sphere = SCNSphere(radius: 10.0)
sphere.segmentCount = 96 // більш сегментів = менше distortion на полюсах
let material = SCNMaterial()
material.isDoubleSided = true
material.diffuse.contents = UIImage(named: "pano.jpg")
sphere.materials = [material]
let sphereNode = SCNNode(geometry: sphere)
sphereNode.scale = SCNVector3(-1, 1, 1) // інверсія нормалей
scene.rootNode.addChildNode(sphereNode)
let camera = SCNCamera()
camera.fieldOfView = 90
let cameraNode = SCNNode()
cameraNode.camera = camera
scene.rootNode.addChildNode(cameraNode)
sceneView.scene = scene
Управління через гіроскоп — CMMotionManager → Euler angles → SCNNode.rotation. Через дотик — UIPanGestureRecognizer з накопленням кута повороту. Інерція при свайпі реалізується через SCNAction з easing, не через фізику — фізика SceneKit додає непередбачуване затухання.
Загрузка великих зображень без OOM
JPEG 50 МПікс після декодування в RGB займає ~150 МБ в пам'яті. На iPhone SE 2nd gen з 3 ГБ RAM це вже третина доступної пам'яті — при фонових процесах отримуєте SIGKILL за memory pressure.
Рішення — тайлова загрузка через CATiledLayer або downscale перед SceneKit:
// Прогресивна загрузка: спочатку превью 2048px, потім повне розширення
let thumbnail = image.preparingThumbnail(of: CGSize(width: 4096, height: 2048))
material.diffuse.contents = thumbnail
// Асинхронно загружаємо повне розширення
Task.detached(priority: .background) {
let full = await loadFullResImage(url: imageURL)
await MainActor.run { material.diffuse.contents = full }
}
preparingThumbnail(of:) — асинхронний API (iOS 15+), декодує в background thread без блокування main run loop.
Підтримка Google Photo Sphere XMP-метаданих
Panorama-камери й Google Street View пишуть у JPEG XMP-метадані з типом проекції й crop-інформацією (GPano:ProjectionType, GPano:CroppedAreaImageWidthPixels). Без їх читання неповна панорама (270° замість 360°) буде показана як повна — з растягнутими краями.
Читаємо через CGImageSourceCopyPropertiesAtIndex → XMP metadata → парсимо GPano:* поля, коригуємо UV-mapping текстури. Це 30–50 рядків коду, але без них третина реальних панорам виглядає неправильно.
Android
На Android використовуємо Filament (Google's physically-based renderer) або Panorama360View через GLSurfaceView. Filament вимагає більш настроєк, але дає коректний tone mapping для HDR-панорам.
Терміни
Базовий просмотрщик (iOS, SceneKit, гіроскоп + дотик): 1–1.5 тижні. Повна реалізація з тайловою загрузкою, XMP-метаданими, iOS + Android: 3–4 тижні. Вартість розраховується індивідуально.







