Представьте ситуацию: на демо клиент испытывает VR-тренажер «Работы на высоте».
Легкий ветерок, стальной пролет, панорама города. Красота. Клиент поднимается по лестнице, останавливается на краю и с восхищением говорит: «Как круто вы сделали, что от вида вниз у меня голова закружилась!» Мы переглядываемся. Потому что «круто» — это не мы сделали. Это заслуга плохой оптимизации раннего прототипа.
Пока персонаж карабкался, движок героически пытался «на лету» подгрузить пачку тяжелых моделей. FPS просел, рендер начал задыхаться, и вестибулярка клиента объявила забастовку. Иммерсивность —10/10, комфорт — где-то в районе отрицательных значений. Если голова кружится, это должно быть запланировано геймдизайнером, а не видеокартой.
Привет, я backend-разработчик SimbirSoft Андрей. В этой статье разберем, как сделать так, чтобы VR-проекты на Unity работали стабильно и были дружелюбны к вестибулярному аппарату игрока.
Кадры решают всё
Основное понятие здесь — FPS (Frames Per Second, частота кадров). Это число кадров, которые игра или приложение отображает на экране каждую секунду. Чем выше FPS, тем плавнее движется картинка и быстрее отклик на действия игрока.
В VR стабильная частота кадров — это не просто метрика производительности, а основа комфорта игрока.
Если в обычной игре кратковременное падение кадров с 90 до 60 игрок может и не заметить, то в VR сбой кадров даже на долю секунды создает рассинхрон между движением и изображением. Мозг получает противоречивые сигналы: к примеру глаза видят движение, а вестибулярный аппарат сообщает, что тело стоит на месте. В результате возникает дискомфорт, головокружение и то самое ощущение укачивания.
Для разных VR-устройств целевые значения FPS отличаются из-за аппаратных ограничений и особенностей дисплеев. Поэтому во время разработки важно проводить тесты производительности на минимальных по характеристикам гарнитурах из списка целевых, чтобы убедиться, что сцена будет стабильно работать даже на самом слабом устройстве с ориентированием на мощность процессора и возможности частот обновления дисплеев. Мы в свое время пытались «на глаз» понять, где у нас узкие места, и это было ошибкой. С тех пор правило одно: Unity Profiler — твой лучший друг.
Кто виноват и что делать
Производительность рендера всегда определяется балансом между двумя ключевыми компонентами: центральным процессором (CPU) и графическим процессором (GPU). Несмотря на то, что оба они работают параллельно, их задачи принципиально различаются.
-
CPU отвечает за выполнение игровой логики, обработку ввода, расчет анимаций, физики, а также подготовку команд на отрисовку объектов (draw calls).
-
GPU же занимается графической частью — обрабатывает вершины и пиксели, применяет шейдеры, текстуры, тени, пост-эффекты и формирует изображение для вывода на экран.
В идеальной ситуации эти два процесса идут синхронно: CPU формирует команды для кадра, передает их в очередь, и пока GPU занимается рендерингом, CPU уже готовит следующий кадр. Но в реальности часто баланс нарушается.
Отследить, кто именно является узким местом, можно через Unity Profiler: если GPU Frame Time выше, чем CPU Frame Time, значит вы упираетесь в графическую производительность. Если наоборот, то проблема в логике и подготовке сцены для отрисовки.
На скриншоте профайлер открыт с аналитикой работы CPU:

-
Main Thread отвечает за игровую логику: расчеты физики, анимаций и других систем. Он формирует, какие объекты и как должны быть отрисованы (команды рендера), но не занимается низкоуровневой подготовкой данных для GPU.
-
Render Thread берет команды, подготовленные Main Thread, и выполняет низкоуровневую подготовку для GPU: переносит данные в буфер, сортирует объекты по материалам и шейдерам, устанавливает состояния рендера (render state changes).
Если CPU не успевает подготовить кадр (расчёты физики, draw calls, обработку объектов и т.д.), в Render Thread появляются маркеры:
Gfx.WaitForGfxCoZmmandsFromMainThread
Semaphore.WaitForSignal
GPU при этом не работает в полную силу, а чилит, как разработчик, который допивает третий кофе и ждет, пока тимлид наконец добавит задачу в Jira.
Пример того, на что можно обратить внимание при такой ситуации:
1. Сокращение количества вызовов рендеринга (Draw Calls)
-
Использовать Static Batching для неподвижных объектов.
-
Применять Dynamic Batching (только для мелких объектов, т.к. может съесть память).
-
GPU Instancing для повторяющихся моделей.
-
Комбинировать мелкие меши в один при загрузке сцены.
2. Оптимизация скриптовой логики
-
Минимизировать код в Update() — выносить логику в события, OnEnable/OnDisable или InvokeRepeating.
-
Кешировать ссылки на компоненты (GetComponent — только при инициализации).
-
Избегать частого выделения памяти (гарbage collector), использовать List.Clear() вместо пересоздания.
-
Переносить тяжелые операции на несколько кадров (корутины или UniTask).
-
Применять Job System + Burst Compiler для параллельных вычислений.
3. Оптимизация физики
-
Уменьшить Fixed Timestep в Project Settings.
-
Использовать простые коллайдеры (Capsule, Box) вместо Mesh Collider.
-
Отключать коллайдеры и Rigidbody у неактивных объектов.
-
Уменьшать количество слоев для физики (Layer Collision Matrix).
-
Объединять триггеры и зоны детекции в крупные объекты.
4. Оптимизация анимаций и IK
-
Отключать анимацию для невидимых объектов (Animator.cullingMode = CullCompletely).
-
Запекать сложные анимации и не использовать IK там, где можно заранее запечь позу.
-
Оптимизировать скелет 3D модели: удалить лишние кости, оставив только необходимые для анимации.
5. Управление обновлением объектов
-
Использовать Object Pooling вместо Instantiate / Destroy.
-
Отключать неиспользуемые объекты (SetActive(false)), а не уничтожать.
-
Делить обновление на группы: часть объектов обновляется раз в 2-3 кадра.
Если же наоборот Main Thread быстро формирует команды, Render Thread также шустро подготавливает их для GPU, а видеокарта не успевает отрисовать кадр, то уже CPU, подготовив все необходимое, ждет освобождения GPU, чтобы передать ему новые данные. В Profiler в таком случае мы можем увидеть флаг Gfx.WaitForPresent. Это значит, что CPU простаивает, пока видеокарта заканчивает текущую работу.

На скриншоте информация чем занят GPU:
-
Растеризацией геометрии (превращение трехмерных моделей в набор пикселей на экране).
-
Вычислением вершинных и пиксельных шейдеров (освещение, тени, материалы, спецэффекты).
-
Постобработкой (Bloom, Depth of Field, Motion Blur и т. п.).
-
Отрисовкой прозрачных объектов и сложных материалов.
-
Выполнением расчетов для теневых карт, отражений и глобального освещения.
-
Сборкой кадра в буфере и подготовкой его к выводу на экран (V-Sync, буферизация).
В этом случае оптимизация смещается на уменьшение визуальной нагрузки:
1. Оптимизация геометрии
-
Сократить количество полигонов, внедрить LOD-системы (LOD Group).
-
Объединить меши для снижения количества draw calls.
-
Использовать Occlusion Culling для отбрасывания невидимых объектов.
2. Оптимизация текстур
-
Снизить разрешение и битность текстур.
-
Использовать сжатия (DXT, ASTC, ETC2 в зависимости от платформы).
-
Использовать атласы для UI и мелких объектов.
3. Оптимизация шейдеров
-
Минимизировать количество ветвлений (if в шейдерах).
-
Убирать ненужные функции (например, Parallax Mapping, если он не критичен).
-
Переходить с Surface Shader на простые Vertex/Fragment, если возможно.
-
Использовать Shader LOD, отключая тяжелые эффекты на слабых устройствах.
4. Оптимизация освещения
-
Бейкить статическое освещение (Baked GI) вместо динамического.
-
Использовать Light Probes для динамических объектов.
-
Уменьшить количество динамических источников света.
-
Сократить дальность и интенсивность теней, использовать мягкие тени только там, где это критично.
5. Оптимизация постобработки
-
Убирать дорогие эффекты (Bloom, SSAO, DOF) или использовать упрощённые версии.
-
Объединять несколько постэффектов в один шейдер (Custom Render Pass).
-
Снижать разрешение рендера постобработки (Half Resolution для SSAO, DOF).
6. Оптимизация прозрачности
-
Минимизировать количество прозрачных объектов (overdraw).
-
Сортировать прозрачные объекты по расстоянию и отбрасывать невидимые.
-
Использовать Cutout вместо Alpha Blend там, где возможно.
7. Масштабирование разрешения рендера
-
Внедрить динамическое масштабирование рендера (Dynamic Resolution Scaling).
-
Использовать TAAU (Temporal Anti-Aliasing Upsample) или FSR.
Приемы и подходы к оптимизации сильно зависят от конкретного проекта, и о них можно написать не одну отдельную статью. Я привел наиболее распространенные и простые кейсы, на которые стоит обратить внимание в первую очередь.
Укрощение диких кадров
Много кадров — это, конечно, хорошо, но даже при 140 FPS одна просадка до 100 превращается в микролаги, и ваш мозг запускает внутренний debugger в лице вестибулярного аппарата, который сообщает о проблемах и эвакуирует содержимое желудка. Что же можно предпринять для исключения подобных ситуаций?
Прежде всего можно начать профилировать пиковые участки и перераспределять нагрузку, чтобы не было резких всплесков на отдельных кадрах.

Вот краткий список простых приемов для снижения пиковых всплесков:
-
Разбивать тяжёлые операции (например, загрузку ассетов или генерацию данных) на несколько кадров с помощью Job System или Coroutines.
-
Иногда полезно зафиксировать частоту на чуть ниже максимальной стабильной (например, вместо нестабильных 140 FPS-ровные 120 FPS). В Unity можно управлять этим через Application.targetFrameRate или Vsync.
-
Избегать GC (Garbage Collector) spikes — следить за аллокациями в Update, использовать пул объектов.
-
Минимизировать частые вызовы FindObjectOfType, GetComponent и других тяжёлых операций.
-
Оптимизировать физику: уменьшить количество объектов с RigidBody/Collider, настроить Fixed Timestep в Project Settings.
-
Избегать внезапных «тяжелых» эффектов (например, взрывов с множеством частиц в один кадр).
-
Снизить дальность и качество теней, если они просаживают производительность в пиковых сценах.
-
Загружать модели, текстуры и анимации до их появления в кадре. Для этого можно использовать Addressables и LoadAssetAsync с буферизацией.
-
Если один из компонентов простаивает, перераспределить работу (например, часть расчетов с CPU на GPU через compute shaders или наоборот).
Два глаза — два бюджета (временного бюджета кадра)
Есть два глаза — один видит идеально точные пики, а другой… должен видеть ровно то же самое, только с небольшим смещением, чтобы создать эффект присутствия.
В VR каждое изображение для игрока формируется с учетом положения и угла обзора левой и правой камер (глаз). Даже если в сцене один объект, он все равно рендерится дважды (по разным траекториям лучей), чтобы сформировать стереопару.
Рассмотрим два режима рендера для такого случая в Unity.
Single Pass Instanced (SPI) позволяет рендерить сразу оба глаза за один проход шейдера с использованием GPU-инстансинга. Шейдер получает данные о положении обоих глаз и формирует стереопару в одном draw call.
Плюсы:
-
Существенно снижает количество draw calls.
-
Снижает нагрузку на CPU и Render Thread.
-
Хорошо поддерживается большинством кастомных шейдеров на PC.
Минусы:
-
Требует поддержки макросов UNITY_VERTEX_INPUT_INSTANCE_ID и UNITY_SETUP_INSTANCE_ID.
-
Некоторые постпроцессинг-эффекты могут работать некорректно.
Когда использовать:
-
PC и современные VR-шлемы с сложными шейдерами.
-
Когда важно снизить нагрузку на CPU и Render Thread.
Multi-View — режим, который заточен больше под мобильные платформы. GPU рендерит оба глаза за один проход, используя возможности драйвера и графического API.
Плюсы:
-
Минимальные требования к шейдерам.
-
Хорошо подходит для мобильного VR.
-
Снижает нагрузку на CPU и Render Thread.
Минусы:
-
Ограниченная поддержка платформ.
-
Иногда сложнее дебажить кастомные эффекты.
-
На PC эффективность зависит от драйвера.
Когда использовать:
-
Мобильные VR-платформы, где поддерживается OpenXR / Vulkan.
-
Сценарии с простыми шейдерами, когда нужно минимизировать draw calls.
Как включить в Unity:
-
Открыть Project Settings → XR Plug-in Management → [ваш провайдер XR].
-
В разделе Stereo Rendering Mode выбрать Single Pass Instanced.

Как не укачать игрока
В этой части рассмотрим геймплейные механики, соблюдение которых помогает избежать дискомфорта у игроков.
Прежде всего, необходимо минимизировать резкие движения камеры. В VR важно, чтобы камера всегда была под контролем игрока — это совсем не то, что происходит в PC-играх, когда после победы камера делает эпичный пролет над полем боя. Представьте, если бы в VR после финального босса ваша камера так же внезапно пролетела над ареной: игрок тут же потерял бы ориентацию, равновесие и уж точно содержимое желудка!
Любые неожиданные скачки или рывки в кадре крайне нежелательны. Если камера движется «обоснованно» для игрока, например, он сидит на вагонетке, которая сама катится по рельсам, изменения угла обзора должны быть плавными и предсказуемыми. Резкие движения повышают риск укачивания и нарушают ощущение присутствия.
Игрок должен заранее понимать, куда и как он движется. Случайные или хаотичные перемещения разрушают восприятие пространства и могут быстро привести к дискомфорту.
Методы передвижения:
Телепорт — самый комфортный и безопасный способ перемещения, особенно для новичков. Он позволяет игроку мгновенно менять положение в виртуальном пространстве, не вызывая дискомфорта вестибулярного аппарата. Конечно, в идеале игрок мог бы передвигаться самостоятельно в реальном пространстве, но, поскольку у большинства пользователей нет свободного «футбольного поля» под рукой, приходится прибегать к виртуальному перемещению.
Smooth locomotion — более «сложный» вариант для вестибулярного аппарата. Здесь игрок перемещается плавно (отклоняя, к примеру, трек джойстика), как при обычной ходьбе, но при этом мозг получает визуальный сигнал движения, а тело остается на месте. Этот разрыв между ощущением движения глазами и отсутствием физического перемещения вызывает эффект укачивания. Чтобы снизить дискомфорт, Smooth locomotion стоит использовать с регулируемой скоростью и предоставлять дополнительные опции для чувствительных к укачиванию пользователей-например, виньетку при движении или ограничение ускорений.
Фиксированные визуальные ориентиры в VR помогают мозгу удерживать положение в пространстве и значительно снижают риск укачивания. К таким ориентирам относятся:
Кабина или cockpit — внутреннее пространство транспорта (самолёта, вагонетки, машины), которое остается стабильным при движении. Кабина создает ощущение «опоры» для глаз и вестибулярного аппарата.
Шлем или элементы HUD — виртуальные интерфейсы, рамки или приборные панели, которые всегда находятся в поле зрения игрока. Они дают мозгу стабильные точки отсчёта при движении и поворотах.
Нос персонажа или virtual nose — тонкая линия или полупрозрачная форма, которая имитирует положение собственного носа игрока. Этот прием помогает мозгу ориентироваться и снижает эффект укачивания, особенно при быстром повороте камеры.
Кроме того, важно не делать FOV слишком широким при движении. Широкий угол обзора усиливает ощущение ускорения и движения в пространстве, что повышает риск дискомфорта. Оптимальный подход — баланс между иммерсией и комфортом: FOV можно немного уменьшать при быстром движении или поворотах, сохраняя при этом достаточную визуальную информацию.
Все параметры движения, поворота и визуальных эффектов должны быть настраиваемыми. Позвольте игроку выбрать метод перемещения, чувствительность поворота и скорость движения под себя потому что у каждого человека свое восприятие и уровень натренированности вестибулярного аппарата. Вход в ваш продукт должен быть постепенный при длительной работе с VR вестибулярный аппарат можно сказать тренируется.
Вместо итогов
В этой статье я разобрал, как сделать VR-проекты на Unity стабильными и дружелюбными к вестибулярному аппарату игрока. Если у вас появились вопросы, напишите их в комментариях.
Спасибо за внимание!
Больше авторских материалов для backend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.
Автор: ArchitectSimbirSoft


