Представьте ситуацию: на демо клиент испытывает 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.
lexnext1
Спасибо за статью!
В основном разработку под vr делаете на pc?
Какую vr-гарнитуру под мак посоветуете для пробы пера в vr-unity?