От статичной панорамы к интерактивной 3D-карте: создаём виртуальный тур на Pannellum
- понедельник, 9 февраля 2026 г. в 00:00:05
Все мы привыкли к Google Street View, но что, если нужно показать пространство, куда машина со 360-камерой не заедет? Интерьер офиса, музей, университетский холл. Коммерческие решения для виртуальных туров часто дороги или ограничены в кастомизации.

Привет! Меня зовут Илья, я студент Иркутского Политеха. Я столкнулся с задачей — создать интерактивную карту этажа университетского корпуса. Ранее создавалась такая же интерактивная карта, но она была не оптимизирована для простого использования для рядового пользователя, который хотел ознакомиться со стенами института, так как данная карта была в виде .exe файла и имела ряд проблем:
Не было возможности работать в веб-версии
Чтобы её посмотреть, нужно было загрузить тяжеловесный файл (чем никто заниматься не будет)
Загрузка каждой фотографии занимала невыносимо долгое время
Приложением можно было пользоваться только на ПК
По задаче нужно было не просто показать панорамы, а связать их в навигационную систему, где можно «переходить» из комнаты в комнату. В этой статье я покажу, как с помощью библиотеки Pannellum и простого JavaScript мы создали полноценный веб-тур с кастомными элементами управления.
После анализа прошлой системы стало понятно, её ключевые недостатки — это десктопоцентричность, тяжеловесность и закрытость — они и стали главными критериями для нового решения. Нам нужна была технология, которая даёт:
1. Доступность из браузера (решает проблемы с .exe и ОС).
2. Быструю загрузку (контраст с «невыносимо долгим» ожиданием).
3. Лёгкость распространения (простая ссылка вместо «тяжеловесного файла»).
Рассматривались варианты:
Three.js + Photo-Sphere-Viewer: Максимальная гибкость, но больше кода для написания «с нуля».
Marzipano: Мощный фреймворк от Google, но с более высоким порогом входа.
Pannellum: Идеальный баланс. Легковесная (~70 КБ), открытая библиотека. Она предоставляет готовый, но кастомизируемый просмотрщик панорам с поддержкой горячих точек (hotspots), гироскопа и, что критично важно, работает на любом устройстве с современным браузером.

Выбор пал на Pannellum, потому что он прямо отвечал на все проблемы старой системы: возможность интеграции в веб-среду, скорость, кроссплатформенность, а также наличие лицензии MIT (которая позволяет свободно работать с библиотекой).

Мы отказались от монолитной программы в пользу простой и понятной веб-архитектуры: одна HTML-страница – одна панорамная локация.
Почему это правильно:
Простота разработки: Каждую комнату можно разрабатывать и тестировать отдельно.
Логика навигации: Переход между локациями – это обычная загрузка страницы по ссылке (<a href> или window.location). Это интуитивно и для пользователя, и для разработчика.
Лёгкость масштабирования: Добавить новую точку – создать новый HTML-файл по готовому шаблону, добавив в него панораму и координаты выходов в другие комнаты.
Производительность: Браузер загружает только одну панораму за раз, что быстро даже на медленных соединениях (в отличие от предзагрузки всех данных в .exe).
2.1 Практика: локальный сервер для разработки и тестирования

Для удобства визуализации результата установим расширение для Visual Studio Code, которое позволяет быстро запустить локальный веб-сервер прямо из редактора, автоматически обновляя страницу браузера при изменении файлов проекта.
Установка: В VS Code откройте панель расширений (Ctrl+Shift+X), найдите «Live Server» от Ritwick Dey и нажмите «Install».

Запуск: После установки в правом нижнем углу редактора появится кнопка «Go Live». Также можно щелкнуть правой кнопкой мыши на любом HTML-файле и выбрать «Open with Live Server».

Результат: Сервер запустится на http://localhost:5500 (порт может меняться), и страница автоматически откроется в браузере. Теперь все функции панорамы будут работать корректно.
Архитектура «одна локация — один HTML-файл» начинается с чистого и понятного шаблона. Давайте разберем его по косточкам, чтобы понять, как каждый элемент готовит почву для интерактивности.
3.1 HTML-шаблон и CSS: скелет и кожа нашей локации
Основной index.html (и любой другой файл локации) служит каркасом. Всю логику кастомизации мы вынесли в отдельные файлы.
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pannellum Example with Tooltip Hotspots</title> <!-- Pannellum из CDN --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css"/> <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"></script> <!-- Наши кастомные модули --> <script src="common.js"></script> <link rel="stylesheet" href="style.css"/> </head> <body> <!-- Контейнер, в котором "живет" панорама --> <div id="pannelleum"></div> <!-- Наши кнопки управления поверх панорамы --> <button class="back-button" onclick="goBack()">Назад</button> <button id="sound_button" class="sound-button sound-button_on" onclick="sound_on_off()"></button> <!-- Скрытый аудиоэлемент --> <audio controls id="audio" src="./audio/Holl.mp3"></audio> <script> // Инициализация панорамы с данными происходит здесь pannellum.viewer('pannelleum', { "type": "equirectangular", "panorama": "Holl.jpg", "autoLoad": true, "hotSpots": [ { "pitch": -14, "yaw": 4, "type": "custom", "createTooltipFunc": hotspot, "createTooltipArgs": { "text": "Холл лестница слева", "URL": "./Holl_lestnitsa_sleva.html" } }, { "pitch": -15, "yaw": 40, "type": "custom", "createTooltipFunc": hotspot, "createTooltipArgs": { "text": "Конференц-зал", "URL": "./K-Konf-zal_tsentr_1.html" } } // … Остальные переходы в локации по тому же принципу … ] }); </script> </body> </html>
Разберём эту часть кода конкретнее:
pannellum.viewer('pannelleum', { "type": "equirectangular", "panorama": "Holl.jpg", "autoLoad": true, "hotSpots": [ { "pitch": -14, "yaw": 4, "type": "custom", "createTooltipFunc": hotspot, "createTooltipArgs": { "text": "Холл лестница слева", "URL": "./Holl_lestnitsa_sleva.html" } }
В этом конфигурационном объекте для Pannellum каждый параметр задаёт ключевые свойства панорамы и навигации:
Основные параметры панорамы:
"type": "equirectangular" — формат проекции панорамы (стандартный для 360° снимков).
"panorama": "Holl.jpg" — путь к файлу фотографии панорамы.
"autoLoad": true — автоматическая загрузка панорамы при открытии страницы.
Параметры горячей точки (hotSpot):
"pitch": -14 — вертикальный угол (в градусах), аналогичный широте. Отрицательное значение = ниже горизонта.
"yaw": 4 — горизонтальный угол (в градусах), аналогичный долготе. Определяет положение по кругу.
"type": "custom" — указывает, что точка будет кастомной (не стандартной иконкой Pannellum).
"createTooltipFunc": hotspot — ссылка на нашу JavaScript-функцию, которая создаёт внешний вид и поведение точки.
"createTooltipArgs": { ... } — объект с аргументами, передаваемыми в функцию hotspot:
"text": "Холл лестница слева" — текст подсказки (tooltip).
"URL": "./Holl_lestnitsa_sleva.html" — цель перехода при клике.
Далее – ключевой момент подключения нашего style.css. Именно этот файл отвечает за интеграцию кастомных элементов с просмотрщиком Pannellum, что было одной из главных технических задач.
Разбор style.css: контроль над интерфейсом
/* style.css */ /* 1. Контейнер для панорамы - основа сцены */ #pannelleum { width: 100%; height: 700px; /* Фиксированная высота для стабильности */ position: relative; /* Создает контекст для позиционирования кнопок! */ } /* 2. Кнопка "Назад" */ .back-button { position: absolute; /* Вырываем из потока и позиционируем */ bottom: 5%; left: 0.7%; padding: 0.65%; background-color: #007bff; color: white; border: none; border-radius: 18%; cursor: pointer; z-index: 1; /* Важно: поднимаем над слоями панорамы */ } /* 3. Кастомная горячая точка (создается в JS) */ .custom-hotspot { width: 120px; height: 70px; background-image: url('hotspot6.png'); /* Наша графика вместо стандартной иконки */ background-size: cover; cursor: pointer; } /* 4. Кнопка звука с двумя состояниями */ .sound-button { position: absolute; top: 1.8%; right: 0.7%; padding: 2.5%; border: none; border-radius: 100%; cursor: pointer; z-index: 1; background-size: cover; background-position: center; } /* Состояния управляются JS через смену класса */ .sound-button_on { background-image: url('./img_tools/Play.png'); } .sound-button_off { background-image: url('./img_tools/Stop.png'); } /* 5. Всплывающая подсказка для горячих точек */ .custom-tooltip { position: fixed; /* Чтобы следовать за курсором по всему экрану */ background: #fff; padding: 8px 12px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 9999; /* Всегда поверх других элементов */ pointer-events: none; /* Чтобы не мешала взаимодействию */ white-space: nowrap; font-family: Arial, sans-serif; font-size: 14px; }
Полный контроль: CSS даёт нам свободу сделать интерфейс уникальным, а не использовать стандартный вид Pannellum.
Абсолютное позиционирование и z‑index: Это решение ключевой проблемы интеграции. Без position: relative у контейнера #pannelleum и position: absolute с z‑index у кнопок, последние «проваливаются» под панораму и становятся некликабельными.
Производительность: Стили для точек (background‑image) загружаются один раз и кешируются браузером.

Немного фотошопа, и наш указатель готов!
3.2 Сердце системы: кастомный JavaScript для навигации и взаимодействия
Вся динамическая логика вынесена в common.js. Этот файл содержит функции управления навигацией, интерактивными элементами и звуком (на некоторых локациях нужно было добавить аудиодорожки).
// common.js // Функция возврата назад function goBack() { window.history.back(); } var sound = false; // Управление звуком function sound_on_off() { if (sound) { document.getElementById('sound_button').className="sound-button sound-button_on"; audio.pause(); } else { document.getElementById('sound_button').className="sound-button sound-button_off"; audio.play().catch(e => console.error("Ошибка воспроизведения:", e)); } sound = !sound; } // Остановка звука после окончания трека audio.addEventListener('ended', function() { sound=false; document.getElementById('sound_button').className="sound-button sound-button_on"; }); // Создаем глобальную переменную для хранения div'а tooltip let tooltipDiv = null; // Обработчик клика на горячую точку function hotspot(hotSpotDiv, args) { // 1. Применяем стиль кастомной точки из CSS hotSpotDiv.classList.add('custom-hotspot'); // 2. Добавляем всплывающую подсказку при наведении hotSpotDiv.addEventListener('mouseenter', (e) => showTooltip(e, args.text)); hotSpotDiv.addEventListener('mouseleave', hideTooltip); // 3. Делаем элемент кликабельным для перехода hotSpotDiv.addEventListener('click', function() { window.location.href = args.URL; }); } // Новая версия функции showTooltip с точным позиционированием function showTooltip(event, text) { if (!tooltipDiv) { tooltipDiv = document.createElement('div'); tooltipDiv.className = 'custom-tooltip'; document.body.appendChild(tooltipDiv); } tooltipDiv.textContent = text; // Точное позиционирование относительно координат мыши const x = event.clientX + window.scrollX + 15; const y = event.clientY + window.scrollY + 15; tooltipDiv.style.left = ${x}px; tooltipDiv.style.top = ${y}px; tooltipDiv.style.display = 'block'; } // Скрываем всплывающую подсказку function hideTooltip() { if (tooltipDiv) { tooltipDiv.style.display = 'none'; } }
Ключевые улучшения в этой реализации:
Глобальный tooltip: Создается один раз и переиспользуется, что эффективнее, чем постоянное создание/удаление DOM-элементов.
Точное позиционирование: Использование event.clientX и event.clientY вместе с window.scrollX/Y гарантирует, что подсказка появится именно рядом с курсором, даже если страница прокручена.
Обработка ошибок аудио: Метод audio.play().catch() перехватывает и логирует ошибки (например, если браузерная политика запрещает автовоспроизведение), предотвращая молчаливые сбои.
Автосброс звука: Событие ended сбрасывает состояние кнопки звука после завершения трека.
Переход на новую технологию не был гладким. Вот основные сложности и как мы их преодолели:
Проблема 1: Кастомные элементы «проваливаются» под панораму.
Горячие точки и кнопки были не кликабельны.
Решение: Контроль за z-index и контекстом позиционирования. Убедились, что у контейнера #pannelleum стоит position: relative, а у наших элементов — position: absolute и высокий z-index (прописано в нашем style.css).
Проблема 2: Координаты горячих точек (pitch, yaw) подбираются наугад.
Визуально разместить точку на двери на панораме — мучительно долго.
Решение: Создали «режим отладки», добавив в конфигурацию Pannellum обработчик клика, который выводит в консоль браузера текущие углы взгляда. Это в разы ускорило расстановку.
Проблема 3: Автовоспроизведение аудио блокируется браузером.
Решение: Добавили обработку ошибок через .catch() в функции sound_on_off(). Теперь, если браузер запрещает автовоспроизведение, ошибка логируется в консоль, а интерфейс не ломается — пользователь может включить звук вручную.

Проблема 4: Изначально размер каждой панорамной фотки занимал огромное кол-во места, доходило аж до 18 мб, чтобы сжать все фотографии, заходим в Фотошоп, Файл -> Сценарии -> Обработчик изображений.

В нём выбираем в по пунктам:
1) Выбрать папку: выбираем папку для сжатия (Там должны находиться все наши фотографии)
2) Выбрать папку: выбираем папку для будущих сжатых фоток
3) Сохраняем как JPEG и выбираем качество по шкале от 1 до 10, я выбрал 4 и добился сжатия самой тяжеловесной фотографии аж в 9 раз, при практически незаметном снижении качества. И, следовательно, увеличиваем скорость загрузки каждой фотографии!


Выводы
Итак, мы получили полнофункциональный веб-тур, остаётся его загрузить на сервер – и он будет открываться по ссылке с любого устройства – телефона, планшета или компьютера. Пользователь может интуитивно «ходить» по этажу, а загрузка каждой новой локации происходит почти мгновенно.
Как мы решили исходные проблемы:
Веб-версия вместо .exe: Да.
Лёгкость доступа: Ссылка вместо тяжёлого файла. Да.
Скорость загрузки: Быстрая загрузка одной панорамы вместо всех сразу: Да.
Кроссплатформенность: Работает на ПК, смартфонах, планшетах. Да.
Технические плюсы подхода:
Полный контроль над интерфейсом и навигацией.
Простота поддержки и расширения карты.
Отличная производительность благодаря легковесности Pannellum.
Устойчивый UX: Обработка ошибок и продуманные состояния элементов.
Минусы и пути развития:
Многофайловость: При большом количестве комнат управлять десятками HTML-файлов становится неудобно. Будущее улучшение: переход на SPA (Single Page Application) с помощью Vue.js/React, где всё состояние карты управляется одним JavaScript-приложением, а данные о панорамах и связях хранятся в едином JSON-файле.
Ручная работа: Подбор координат точек, хотя и ускоренный, остаётся ручным. В идеале — создать простой визуальный редактор.
Доработка адаптивности: Фиксированная высота панорамы (700px) не всегда оптимальна для всех мобильных устройств. Можно добавить медиа-запросы для динамического расчета высоты.
Подводя итоги, используя открытую библиотеку Pannellum и стандартные веб-технологии (HTML, CSS, JS), можно за относительно короткое время создать интерактивную 3D-карту, которая соответствует современным ожиданиям пользователей по доступности и удобству.
Ссылка на гитхаб (Проект выполнен в тестовом виде)