Реализация VR-взаимодействия через Gaze (направление взгляда) в мобильном приложении
В мобильном VR без контроллеров гейз-интерфейс — единственный способ взаимодействовать с миром. Кажется простым: смотри на кнопку, она нажимается. На практике неправильно реализованный gaze раздражает пользователя быстрее, чем любой другой UI-паттерн.
Raycast из центра взгляда
Gaze определяется направлением взгляда камеры. Ray пускается из позиции камеры вперёд по Camera.main.transform.forward:
void FixedUpdate() {
Ray gazeRay = new Ray(Camera.main.transform.position,
Camera.main.transform.forward);
if (Physics.Raycast(gazeRay, out RaycastHit hit, maxGazeDistance, interactableLayer)) {
var target = hit.collider.GetComponent<IGazeTarget>();
if (target != null) {
HandleGazeHit(target, hit.point);
} else {
HandleGazeMiss();
}
} else {
HandleGazeMiss();
}
}
FixedUpdate() вместо Update() — стабильная частота вызовов не привязана к FPS. На слабых устройствах при просадках до 30 FPS Update() даёт неравномерный отклик.
Слой interactableLayer — обязательно. Raycast по всей сцене дорого, и пользователь не должен случайно активировать невидимые коллайдеры.
Reticle (курсор взгляда)
Reticle — визуальный индикатор точки взгляда. Размещается в world space на поверхности объекта под взглядом. Расстояние динамическое: reticle «прилипает» к хитпойнту.
void UpdateReticle(Vector3 hitPoint, Vector3 hitNormal) {
reticleTransform.position = hitPoint + hitNormal * RETICLE_OFFSET;
reticleTransform.rotation = Quaternion.LookRotation(-hitNormal);
// Масштаб постоянный в угловых единицах (constant apparent size)
float dist = Vector3.Distance(Camera.main.transform.position, hitPoint);
reticleTransform.localScale = Vector3.one * dist * ANGULAR_SIZE;
}
Когда объекта под взглядом нет — reticle на дефолтном расстоянии (3–5 метров). Не прячем его: пользователь всегда должен видеть, куда смотрит.
Dwell-активация и прогресс-индикатор
Пользователь смотрит на объект N секунд — происходит активация. Оптимальное время дwell: 1.2–2.0 секунды. Меньше 1 секунды — случайные активации при обзоре сцены. Больше 2 секунд — утомляет.
Прогресс должен быть заметен. Заполняющееся кольцо вокруг reticle — стандарт:
public class GazeDwellController : MonoBehaviour {
[SerializeField] private float dwellTime = 1.5f;
[SerializeField] private Image progressRing;
private float dwellProgress = 0f;
private IGazeTarget currentTarget;
private bool isActivated = false;
public void OnGazeEnter(IGazeTarget target) {
currentTarget = target;
dwellProgress = 0f;
isActivated = false;
progressRing.gameObject.SetActive(true);
}
public void OnGazeStay() {
if (isActivated) return;
dwellProgress += Time.deltaTime / dwellTime;
progressRing.fillAmount = dwellProgress;
if (dwellProgress >= 1f) {
isActivated = true;
currentTarget?.OnGazeActivate();
StartCoroutine(ResetAfterDelay(0.5f));
}
}
public void OnGazeExit() {
currentTarget = null;
progressRing.gameObject.SetActive(false);
dwellProgress = 0f;
}
}
После активации — короткий cooldown перед следующей активацией того же объекта (0.5–1.0 сек). Иначе пользователь не успевает убрать взгляд и кнопка «нажимается» дважды.
Hover-состояние: обратная связь до активации
Когда пользователь смотрит на объект, но dwell ещё не завершён, нужна немедленная визуальная обратная связь. Объект должен как-то отреагировать при OnGazeEnter — до истечения времени активации. Варианты:
- Подсветка: изменение emission цвета материала
- Масштаб: объект слегка увеличивается (
0.05fхватает) - Анимация: иконка реагирует на взгляд
- Звуковой сигнал: короткий click при начале dwell
Без этого пользователь не понимает, «видит» ли его приложение.
Cardboard button как подтверждение
У Cardboard есть физическая кнопка (магнитный триггер). Добавляем её как альтернативный метод активации вместо dwell — для продвинутых пользователей это быстрее и удобнее:
// Cardboard SDK trigger event
void Update() {
if (CardboardInput.GetButtonDown()) {
TriggerCurrentGazeTarget();
}
}
Кнопка — не замена dwell, а дополнение. Не все корпусы Cardboard имеют рабочую магнитную кнопку.
Типичные ошибки реализации
Слишком маленький коллайдер у интерактивного объекта — пользователь «промахивается» мимо кнопки. Коллайдер должен быть на 10–20% больше видимого объекта.
Активация срабатывает при любом перемещении взгляда мимо объекта — не только при намеренной фиксации. Решается минимальным порогом угловой скорости головы при старте dwell.
Reticle трясётся из-за дрожания рук — gyro-fusion от Cardboard SDK сглаживает это, но дополнительный Lerp на позицию reticle (~20ms) убирает остаточное дрожание.
Процесс работы
Анализ интерактивных элементов: типы объектов, сценарии взаимодействия.
Реализация raycast-системы с правильными слоями и коллайдерами.
Reticle в world space с constant apparent size.
Dwell controller с прогресс-индикатором, hover-состоянием, cooldown.
Cardboard button как альтернативный триггер.
Тестирование комфорта: время dwell, размеры кнопок, обратная связь.
Ориентиры по срокам
Базовая gaze interaction система с reticle и dwell — 3–5 дней. Полноценная система с несколькими типами интерактивных объектов, анимациями, звуком и настраиваемыми параметрами — 1–2 недели.







