Реализация просмотра 360°-фотографий в мобильном приложении
360°-фото — это equirectangular-изображение: сферическая проекция, где 360° по горизонтали и 180° по вертикали упакованы в прямоугольник с соотношением сторон 2:1. На телефоне его нужно «развернуть» обратно в сферу и дать пользователю смотреть изнутри. Задача не в том, чтобы показать картинку — задача в том, чтобы сфера рендерилась без distortion-артефактов и скроллинг не лагал даже на 50 МПикс JPEG.
Почему стандартный UIImageView или ImageView не работают
UIImageView отображает плоское изображение. Для сферической проекции нужен 3D-рендер: сфера с инвертированными нормалями (чтобы смотреть изнутри), equirectangular-текстура на неё, камера в центре, управление поворотом через gyroscope или touch.
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, gyroscope + touch): 1–1.5 недели. Полная реализация с тайловой загрузкой, XMP-метаданными, iOS + Android: 3–4 недели. Стоимость рассчитывается индивидуально.







