VR Gaze Interaction у мобільних додатках
У мобільному VR без контролерів gaze-інтерфейс — єдиний спосіб взаємодіяти зі світом. Здається простим: дивись на кнопку, вона натискається. На практиці неправильно реалізований gaze розсердить користувача швидше, ніж будь-якої інший UI-паттерн.
Gaze 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 "прилипає" до hit point.
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-активація та progress-індикатор
Користувач дивиться на об'єкт N секунд — відбувається активація. Оптимальний час dwell: 1,2–2,0 секунди. Менше 1 секунди — випадкові активації при огляді сцени. Більше 2 секунд — утомлює.
Progress повинен бути помітним. Заповнюючееся кільце навколо 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 з progress-індикатором, hover-станом, cooldown.
Cardboard button як альтернативний триггер.
Тестування комфорту: час dwell, розміри кнопок, зворотній зв'язок.
Оцінка часу
Базова gaze interaction система з reticle та dwell — 3–5 днів. Повнофункціональна система з кількома типами інтерактивних об'єктів, анімаціями, звуком та налаштовуваними параметрами — 1–2 тижні.







