«Веяния моды привносят в жизнь новые проблемы заставляют меняться» Наверно именно от этой мысли было принято решение подключить в проект на Unity, шлем виртуальной реальности, всем известный Oculus Rift DK2. Вопреки суровому прощупыванию рублем финансового дна удалось заказать Oculus Rift с доставкой в Санкт-Петербург по адекватной цене. Оперативно, менее чем за две недели, заказ прибыл в стены нашего офиса.

В коробке, как и предполагалось лежал сам шлем, набор необходимых кабелей, 2 комплекта линз и камера позиционирования шлема в пространстве. После распаковки и подключения к тестовому компьютеру сразу обнаружилась первая странность. Шлем отказывался работать в режиме Direct Display, прекрасно чувствуя себя в режиме второго монитора. Причем данная особенность наблюдалась только на тестовом компьютере. В качестве решения было принято множество адекватных и не очень решений в виде переустановки драйверов, установки недостающих Microsoft Visual C++ Redistributable и прочих “нужных” приложений и библиотек. После переустановки Windows шлем по-прежнему работал только в режиме расширенного дисплея. Но мудрый коллега установил на тестовый компьютер все доступные на тот момент обновления Windows, за что ему огромное спасибо. И одно, но самое нужное, из более тысячи установленных обновлений решало проблему, шлем заработал в режиме Direct Mode.
Наконец-то, можно было приступить к вкусному —играм тестированию возможностей. Первое впечатление — “ВАУ”. Мозг активно утверждал, что все реально и можно даже потрогать. Словами это не описать, лучше попробовать.
Опустим лирику, пора приступать к серьезным вещам — интеграции шлема в проект на движке Unity.
Первым делом скачан официальные пакет Oculus Unity 4 Integration, наиболее актуальной версии. Разработчикам пакета очень хочется сказать, спасибо, префаб плеера сделан на отлично, несколько кликов позволяет погрузится в виртуальную реальность своего проекта. Только вот изображения и определение позиции и поворот головы для полноценного проекта недостаточно, необходимо сделать несколько вещей:
Приступая к реализации в качестве исходного префаба ��ыл взят — OvrCameraRig, находящийся в официальном пакете к Unity.
После экспериментов и переделывания всего используемого интерфейса, а его предостаточно, наиболее оптимальным выбрано направление — получать изображение с камеры интерфейса в текстуру, затем отображать её перед игроком. Добавив новый класс, отвечающий за интеграцию шлема в проект. В нем появились первые строчки кода, позволяющий по маске слоя интерфейса найти нужную камеру и получать с нее изображение.
Изначально картинка с интерфейсом отображалась на обычной прямоугольной плоскости, но при использовании шлема возникал дискомфорт. Были испробованы различные варианты формы поверхности, самым приятным глазу стала изогнутая плоскость, на подобии современных изогнутых телевизоров. Плоскость с нужными значениями была вынесена в отдельный префаб, но можно этого не делать и пропустить участок кода с выставлением позиции плоскости. Я рекомендую выбрать такие параметры позиции плоскости перед игроком, чтобы пользователь, приближая голову не смог посмотреть, как выглядит плоскость сзади, но и не слишком далеко, чтобы в случае можно было приблизить голову и прочитать что написано. В результате при запуске получилась примерно вот такая картина.

Для отображения плоскости с изображением меню поверх всех объектов окружения, необходимогуглить писать шейдер который всегда будет рисовать себя поверх всех. В мануале написания шейдеров к движку Unity в статье «ShaderLab syntax: Culling & Depth Testing» описано что в проход шейдера добавить параметр ZTest Always и будет счастье шейдер будет рисовать, как и планировалось. Выбрав первый попавшийся не освещаемый шейдер, я использовал шейдер, поставляемый совместно с NGUI, копируем его, даем новое имя и добавляем параметр ZTest.
Вид из редактора позволяет увидеть, что плоскость хоть и пересекает стену строения, но изображение все равно рисуется последним.

А так это выглядит в шлеме:

Результат работы мне понравился, как по ресурсам, так и по виду. Отбросив виртуальную реальность в сторону, можно налить себе очередную чашечку кофе и поболтать с коллегами о бытии мирском.
«Откуда куда зачем все это непонятно. Сидел бы пил кофе, смотрел бы в плоский монитор, не сильно то и нужна мне эта виртуальная реальность. Хотя кого я обманываю, конечно же нужна»
Никогда не думал, что отображать курсор будет сложно, но для шлема это оказалась весьма интересная задачка. Самое первое и очевидное рисовать курсор на обычном интерфейсе и отображать его на плоскости перед игроком.
UIPanel, UISprite или UITexture, получается курсор. Красиво, изящно, просто. Но в шлеме все совершенно иначе. Двигаем мышкой курсор — двигается, наводим на элемент интерфейса — реагирует, отлично, даже не верится. Наводим курсор на пустую область меню, смотрим в пространство и мозг пытается фокусироваться на объекте в пространстве, но какая-то мушка на стекле, курсор, мешает это сделать, либо курсор раздваивается, либо пространство впереди. Конечно можно сделать так чтобы меню исчезало и появлялось по желанию пользователя. Делается это добавлением парой строк кода и несколькими дополнительными свойствами.
Возможно этого будет достаточно в тех проектах где меню не используется в игровом мире. Но текущий проект подразумевал другое использование меню и поиски решений продолжились.
Была опробована идея “лазерной указки”. Привязать к плееру новый объект с источником света, выставить следующие параметры.

И мечта котика, светящаяся точка, перемещается по виртуальному миру. В шлеме глаз нарадоваться не может. Точка аккурат на том объекте куда указывает и никакого дискомфорта. Наигравшись с “лазерной указкой” в виртуальном мире, возвращаюсь в реальный и понимаю, решение не совсем подходящее. Заменив источник света на прожектор получается красивый курсор, он может быть не только светящейся точкой, но и любой картинкой которая есть в наличии.

Плюс в материал прожектора необходимо вставить следующий шейдер:
У прожектора есть небольшое ограничение, он невидим на скайбоксе. Для исправления данной особенности, я решил сделать следующее:
Вот такой результат получился в итоге:


В результате есть курсор, который видим в пространстве, но никак не видим в меню и наоборот, есть в меню, но вызывает дискомфорт при его отображении в пространстве.
Была идея написать для камер шейдер рисующий картинку курсора для глаза с сдвигом к носу в зависимости от ZDepth. Но идея закончилась на том что, мои знания в области написания шейдеров ограничены, и как это сделать я не представляю. Может, кто, в комментариях натолкнет на мысль, как реализовать данную задумку.
Оставив все как есть, я взялся за определение позиции курсора. В распоряжении имеется позиция курсора согласно указателю мыши, есть значения поворота и положения головы. Как наиболее адекватно управлять курсором совершенно не понятно. «Истина рождается в споре.» После небольшого обсуждения с коллегами их мнения разделились, одна половина говорила, что двигать курсором лучше мышкой, другая — лучше головой. «Лучше один раз попробовать а потом обсуждать.»
Первый вариант — проще сделать для курсора в меню.
Позиция курсора Input.mousePosition, переводится в координаты и согласно этим координатам двигается курсор.
Второй вариант хорошо подходит для курсора в пространстве.
Прожектор сделать дочерним к взгляду, и курсор теперь управляется головой.
Итог, возможность использовать два курсора, один управляется мышью и используется в меню, другой управляется головой и используется в пространстве. Вроде неплохо, сохраняю каждый курсор в отдельные префабы для динамического создания и использования в дальнейшем и добавляю следующий код в класс интеграции шлема.
Но два разных курсора, да еще и которые управляются по-разному, это мне показались халтурным исполнением.
«Что нам стоит дом построить, нарисуем будем жить»
Половина пути пройдено. Надо вычислять и возвращать луч. Одна очень хорошая мысль, моментально, посетила мою бедную голову, жалко это происходит не регулярно. Необходимо написать для камеры расширение, которое имело следующий вызов, Camera.main.ExternalScreenPointToRay, и возвращало новый луч. Для этого необходим код:
Добавлен статичный флаг о возможности использовать вычисления позиции шлема.
А также добавления статичной ссылки на экземпляр класса
Не забываем в функции Start() присваивать им значения
Такой синглтон получился.
Для переключения режимом вычисления значении луча я создал следующее перечисление. Ох, и люблю я это делать, плодить перечисления.
В качестве отправной точки я решил взять следующее условие: если камера трехмерная — луч это туда куда направлен прожектор, если двухмерная – луч — это позиция где пересекается прожектор и плоскость на которой отображается рендер меню. Т.е. ��ычисления примут примерно следующий вид:
А код расширения камер примет вот такой вид:
Для вычисления луча трехмерного курсора все предельно понятно:
Для вычисления позиции двухмерного луча необходимо найти пересечение прожектора и плоскости. Это легко посчитать, используя RaycastHit.textureCoord. Предварительно к плоскости добавлен Mesh Collider и она выделена в отдельный слой.
Немного добавил изменение позиции курсора согласно выбранному режиму в функцию Update().
Ну и еще свойство для удобства:
Теперь заменив все вызовы ScreenPointToRay на ExternalScreenPointToRay курсор синхронно двигается в меню и в пространстве, смотреть одно загляденье. Правда есть маленький минус. Курсор теперь виден на плоскости и в пространстве одновременно. Немного преобразив код шедера плоскости с изображением интерфейса, убираем полупрозрачность.
И финальный штрих, в интерфейсе на месте где должен отображаться курсор вешается коллайдер и проверяя находится ли двухмерный курсор над коллайдером интерфейса отображаем его, либо прячем в противном случае.
Вот вроде и все, есть возможность навигации по меню, есть возможность отображать курсор там куда будет фокусироваться зрение, есть возможность вычислять луч для Raycast.
Какие я хочу дать рекомендации для оптимизации проекта под виртуальный мир на основе полученного опыта.
P.S. Наверно некоторые подумают, что это бред и почему для пространственного курсора я не использовал более дешевый прием с райкастом и рисованием билборда направленного в сторону плеера с вычислением его размера относительно дальности. На сцене в используемом проекте не везде стоят коллайдеры и некоторые коллайдеры не соответствуют размерам объекта и в итоге получается, что курсор иногда висит непонятно где и как.

Общее впечатление
В коробке, как и предполагалось лежал сам шлем, набор необходимых кабелей, 2 комплекта линз и камера позиционирования шлема в пространстве. После распаковки и подключения к тестовому компьютеру сразу обнаружилась первая странность. Шлем отказывался работать в режиме Direct Display, прекрасно чувствуя себя в режиме второго монитора. Причем данная особенность наблюдалась только на тестовом компьютере. В качестве решения было принято множество адекватных и не очень решений в виде переустановки драйверов, установки недостающих Microsoft Visual C++ Redistributable и прочих “нужных” приложений и библиотек. После переустановки Windows шлем по-прежнему работал только в режиме расширенного дисплея. Но мудрый коллега установил на тестовый компьютер все доступные на тот момент обновления Windows, за что ему огромное спасибо. И одно, но самое нужное, из более тысячи установленных обновлений решало проблему, шлем заработал в режиме Direct Mode.
Наконец-то, можно было приступить к вкусному —
Интеграция в проект
Опустим лирику, пора приступать к серьезным вещам — интеграции шлема в проект на движке Unity.
Первым делом скачан официальные пакет Oculus Unity 4 Integration, наиболее актуальной версии. Разработчикам пакета очень хочется сказать, спасибо, префаб плеера сделан на отлично, несколько кликов позволяет погрузится в виртуальную реальность своего проекта. Только вот изображения и определение позиции и поворот головы для полноценного проекта недостаточно, необходимо сделать несколько вещей:
- отображать интерфейс пользователя;
- показывать курсор;
- вычислять луч с камер;
Приступая к реализации в качестве исходного префаба ��ыл взят — OvrCameraRig, находящийся в официальном пакете к Unity.
Отображение интерфейса пользователя
После экспериментов и переделывания всего используемого интерфейса, а его предостаточно, наиболее оптимальным выбрано направление — получать изображение с камеры интерфейса в текстуру, затем отображать её перед игроком. Добавив новый класс, отвечающий за интеграцию шлема в проект. В нем появились первые строчки кода, позволяющий по маске слоя интерфейса найти нужную камеру и получать с нее изображение.
[SerializeField] private string guiLayerName; [SerializeField] private string guiLayerPlaneName; [SerializeField] private Color backgroundColor = new Color(0, 0, 0, 0.5f); [SerializeField] private GameObject guiPlanePrefab = null; private RenderTexture _guiRenderTexture = null; private RenderTexture guiRenderTexture { get { if (null == _guiRenderTexture) { _guiRenderTexture = new RenderTexture(Screen.width, Screen.height, 0); } return _guiRenderTexture; } } private Transform centerEyeAnchor = null; private Camera guiCamera = null; private int guiLayer = 0; private int guiLayerPlane = 0; private GameObject guiPlane = null; private void Start() { guiLayer = LayerMask.NameToLayer(guiLayerName); guiLayerPlane = LayerMask.NameToLayer(guiLayerPlaneName); guiCamera = NGUITools.FindCameraForLayer(guiLayer); centerEyeAnchor = GetComponent<OVRCameraRig>().centerEyeAnchor; if (null != guiCamera) { guiRootPanel = guiCamera.GetComponentInParent<UIPanel>(); guiCamera.targetTexture = guiRenderTexture; if (null != guiPlanePrefab) { guiPlane = Instantiate(guiPlanePrefab) as GameObject; guiPlane.layer = guiLayerPlane; guiPlane.renderer.material.mainTexture = guiRenderTexture; Vector3 ls = guiPlane.transform.lossyScale; Vector3 lp = guiPlane.transform.position; Quaternion lr = guiPlane.transform.rotation; guiPlane.transform.parent = transform; guiPlane.transform.localScale = ls; guiPlane.transform.localPosition = lp; guiPlane.transform.localRotation = lr; } } else { throw new UnityException(string.Format("Camera for layer {0} not found", guiLayer)); } } private void OnGUI() { RenderTexture previousActive = RenderTexture.active; RenderTexture.active = guiRenderTexture; GL.Clear(false, true, backgroundColor ); RenderTexture.active = previousActive; guiCamera.Render(); }
Изначально картинка с интерфейсом отображалась на обычной прямоугольной плоскости, но при использовании шлема возникал дискомфорт. Были испробованы различные варианты формы поверхности, самым приятным глазу стала изогнутая плоскость, на подобии современных изогнутых телевизоров. Плоскость с нужными значениями была вынесена в отдельный префаб, но можно этого не делать и пропустить участок кода с выставлением позиции плоскости. Я рекомендую выбрать такие параметры позиции плоскости перед игроком, чтобы пользователь, приближая голову не смог посмотреть, как выглядит плоскость сзади, но и не слишком далеко, чтобы в случае можно было приблизить голову и прочитать что написано. В результате при запуске получилась примерно вот такая картина.

Для отображения плоскости с изображением меню поверх всех объектов окружения, необходимо
Pass { Cull Off Lighting On ZWrite Off ZTest Always Fog { Mode Off } Offset 0, -1 Blend SrcAlpha OneMinusSrcAlpha CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" ... }
Вид из редактора позволяет увидеть, что плоскость хоть и пересекает стену строения, но изображение все равно рисуется последним.

А так это выглядит в шлеме:

Результат работы мне понравился, как по ресурсам, так и по виду. Отбросив виртуальную реальность в сторону, можно налить себе очередную чашечку кофе и поболтать с коллегами о бытии мирском.
Курсор
«Откуда куда зачем все это непонятно. Сидел бы пил кофе, смотрел бы в плоский монитор, не сильно то и нужна мне эта виртуальная реальность. Хотя кого я обманываю, конечно же нужна»
Никогда не думал, что отображать курсор будет сложно, но для шлема это оказалась весьма интересная задачка. Самое первое и очевидное рисовать курсор на обычном интерфейсе и отображать его на плоскости перед игроком.
UIPanel, UISprite или UITexture, получается курсор. Красиво, изящно, просто. Но в шлеме все совершенно иначе. Двигаем мышкой курсор — двигается, наводим на элемент интерфейса — реагирует, отлично, даже не верится. Наводим курсор на пустую область меню, смотрим в пространство и мозг пытается фокусироваться на объекте в пространстве, но какая-то мушка на стекле, курсор, мешает это сделать, либо курсор раздваивается, либо пространство впереди. Конечно можно сделать так чтобы меню исчезало и появлялось по желанию пользователя. Делается это добавлением парой строк кода и несколькими дополнительными свойствами.
[SerializeField] private KeyCode showMenuKey = KeyCode.None; [SerializeField] private float displayTime = 0.61f; private float alphaCalculate = 1; private UIPanel guiRootPanel = null; private Start() { … guiRootPanel = guiCamera.GetComponentInParent<UIPanel>(); … } private void OnGUI() { RenderTexture previousActive = RenderTexture.active; RenderTexture.active = guiRenderTexture; if (null != guiRootPanel) guiRootPanel.alpha = alphaCalculate; Color color = backgroundColor * new Color(1, 1, 1, alphaCalculate); GL.Clear(false, true, color); RenderTexture.active = previousActive; guiCamera.Render(); } private void LateUpdate() { if (Input.GetKeyDown(showMenuKey)) { menuIsShow = !menuIsShow; StopCoroutine("LerpAlpha"); StartCoroutine(“LerpAlpha”, (menuIsShow ? 1 : 0)); } } private IEnumerator LerpAlpha(float endAlpha) { float t = 0; float time = Mathf.Abs(endAlpha - alphaCalculate) / displayTime; while (t < time) { t += Time.deltaTime; alphaCalculate = Mathf.Lerp(alphaCalculate, endAlpha, t / time); yield return null; } }
Возможно этого будет достаточно в тех проектах где меню не используется в игровом мире. Но текущий проект подразумевал другое использование меню и поиски решений продолжились.
Была опробована идея “лазерной указки”. Привязать к плееру новый объект с источником света, выставить следующие параметры.

И мечта котика, светящаяся точка, перемещается по виртуальному миру. В шлеме глаз нарадоваться не может. Точка аккурат на том объекте куда указывает и никакого дискомфорта. Наигравшись с “лазерной указкой” в виртуальном мире, возвращаюсь в реальный и понимаю, решение не совсем подходящее. Заменив источник света на прожектор получается красивый курсор, он может быть не только светящейся точкой, но и любой картинкой которая есть в наличии.
Плюс в материал прожектора необходимо вставить следующий шейдер:
Shader"Projector/Additive"{ Properties{ _ShadowTex("Cookie",2D)=""{TexGenObjectLinear} } Subshader{ Pass{ CullBack ZWriteOff Color[_Color] ColorMaskRGB BlendSrcAlphaOneMinusSrcAlpha Offset0,0 SetTexture[_ShadowTex]{ constantColor(1,1,1,1) combinetexture*constant,texture Matrix[_Projector] } } } }
У прожектора есть небольшое ограничение, он невидим на скайбоксе. Для исправления данной особенности, я решил сделать следующее:
- добавил новый объект дочерний к прожектору;
- на этот объект добавил компоненты: MeshRenderer, Mesh;
- выбрал не освещаемый материал с изображением курсора;
- выставил согласно длины и размера прожектора.
Вот такой результат получился в итоге:


В результате есть курсор, который видим в пространстве, но никак не видим в меню и наоборот, есть в меню, но вызывает дискомфорт при его отображении в пространстве.
Была идея написать для камер шейдер рисующий картинку курсора для глаза с сдвигом к носу в зависимости от ZDepth. Но идея закончилась на том что, мои знания в области написания шейдеров ограничены, и как это сделать я не представляю. Может, кто, в комментариях натолкнет на мысль, как реализовать данную задумку.
Оставив все как есть, я взялся за определение позиции курсора. В распоряжении имеется позиция курсора согласно указателю мыши, есть значения поворота и положения головы. Как наиболее адекватно управлять курсором совершенно не понятно. «Истина рождается в споре.» После небольшого обсуждения с коллегами их мнения разделились, одна половина говорила, что двигать курсором лучше мышкой, другая — лучше головой. «Лучше один раз попробовать а потом обсуждать.»
Первый вариант — проще сделать для курсора в меню.
Позиция курсора Input.mousePosition, переводится в координаты и согласно этим координатам двигается курсор.
Второй вариант хорошо подходит для курсора в пространстве.
Прожектор сделать дочерним к взгляду, и курсор теперь управляется головой.
Итог, возможность использовать два курсора, один управляется мышью и используется в меню, другой управляется головой и используется в пространстве. Вроде неплохо, сохраняю каждый курсор в отдельные префабы для динамического создания и использования в дальнейшем и добавляю следующий код в класс интеграции шлема.
[SerializeField] private GameObject cursor3dPrefab = null; [SerializeField] private GameObject cursor2dPrefab = null; private GameObject cursor3d = null; private GameObject cursor2d = null; private Start() { ... if (null != cursor3dPrefab) { cursor3d = Instantiate(cursor3dPrefab) as GameObject; cursor3d.transform.parent = centerEyeAnchor; cursor3d.transform.localPosition = Vector3.zero; cursor3d.transform.localRotation = Quaternion.identity; cursor3d.transform.localScale = Vector3.one; } if (null != cursor2dPrefab) { cursor2d = Instantiate(cursor2dPrefab) as GameObject; cursor2d.transform.parent = guiCamera.transform; cursor2d.transform.localPosition = Vector3.zero; cursor2d.transform.localRotation = Quaternion.identity; cursor2d.transform.localScale = Vector3.one; UITexture texture = cursor2d.GetComponentInChildren<UITexture>(); if (null != texture) cursor2d = texture.gameObject; } ... }
Но два разных курсора, да еще и которые управляются по-разному, это мне показались халтурным исполнением.
ScreenPointToRay для Raycast
«Что нам стоит дом построить, нарисуем будем жить»
Половина пути пройдено. Надо вычислять и возвращать луч. Одна очень хорошая мысль, моментально, посетила мою бедную голову, жалко это происходит не регулярно. Необходимо написать для камеры расширение, которое имело следующий вызов, Camera.main.ExternalScreenPointToRay, и возвращало новый луч. Для этого необходим код:
public static class ExternalCamera { public static RayExternalScreenPointToRay(thisCameracamera,Vector3position){ return camera.ScreenPointToRay(position); } }
Добавлен статичный флаг о возможности использовать вычисления позиции шлема.
public static bool useOVR { get; private set; }
А также добавления статичной ссылки на экземпляр класса
public static ExtensionOVR instance { get; private set; }
Не забываем в функции Start() присваивать им значения
instance = this; useOVR = true;
Такой синглтон получился.
Для переключения режимом вычисления значении луча я создал следующее перечисление. Ох, и люблю я это делать, плодить перечисления.
public enum CameraRay { Head, Cursor }
В качестве отправной точки я решил взять следующее условие: если камера трехмерная — луч это туда куда направлен прожектор, если двухмерная – луч — это позиция где пересекается прожектор и плоскость на которой отображается рендер меню. Т.е. ��ычисления примут примерно следующий вид:
public Ray ScreenPointToRay(Camera camera, Vector2 position) { return camera.orthographic ? guiPointToRay : headPointToRay; }
А код расширения камер примет вот такой вид:
public static Ray ExternalScreenPointToRay(this Camera camera, Vector3 position) { return ExtensionOVR.useOVR ? ExtensionOVR.instance.ScreenPointToRay(camera, position) : camera.ScreenPointToRay(position); }
Для вычисления луча трехмерного курсора все предельно понятно:
private Ray headPointToRay { get { return (cameraRay == CameraRay.Cursor && null != cursor3d) ? new Ray(cursor3d.transform.position, cursor3d.transform.forward) : new Ray(centerEyeAnchor.position, centerEyeAnchor.forward); } }
Для вычисления позиции двухмерного луча необходимо найти пересечение прожектора и плоскости. Это легко посчитать, используя RaycastHit.textureCoord. Предварительно к плоскости добавлен Mesh Collider и она выделена в отдельный слой.
public Vector2 cursorPosition { get; private set; } private Ray guiPointToRay { get { RaycastHit hit; if (Physics.Raycast(headPointToRay, out hit, 1000, 1 << guiLayerPlane)) { cursorPosition = new Vector2(hit.textureCoord.x * Screen.width, hit.textureCoord.y * Screen.height); return guiCamera.ScreenPointToRay(cursorPosition); } else { return new Ray(); } } }
Немного добавил изменение позиции курсора согласно выбранному режиму в функцию Update().
if (cameraRay == CameraRay.Cursor && null != cursor3d) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); cursor3d.transform.LookAt(cursor3d.transform.position + ray.direction); } if (null != cursor2d) { cursor2d.transform.localPosition = cursorPositionOffset; }
Ну и еще свойство для удобства:
public Vector2 cursorPositionOffset { get { return cursorPosition + offsetCursor; } }
Теперь заменив все вызовы ScreenPointToRay на ExternalScreenPointToRay курсор синхронно двигается в меню и в пространстве, смотреть одно загляденье. Правда есть маленький минус. Курсор теперь виден на плоскости и в пространстве одновременно. Немного преобразив код шедера плоскости с изображением интерфейса, убираем полупрозрачность.
v2f vert (appdata_t v) { ... o.color.a = v.color.a > 0 ? 3 : 0; ... }
И финальный штрих, в интерфейсе на месте где должен отображаться курсор вешается коллайдер и проверяя находится ли двухмерный курсор над коллайдером интерфейса отображаем его, либо прячем в противном случае.
public bool enable2DCursor { get { return Physics.Raycast(guiCamera.ScreenPointToRay(cursorPosition), float.MaxValue, 1 << guiLayer); } } private void Update() { ... if (enable2DCursor) { cursor2d.transform.localPosition = cursorPositionOffset; } else { cursor2d.transform.localPosition = Vector3.up * 10000; } ... }
Вот вроде и все, есть возможность навигации по меню, есть возможность отображать курсор там куда будет фокусироваться зрение, есть возможность вычислять луч для Raycast.
Итог
Какие я хочу дать рекомендации для оптимизации проекта под виртуальный мир на основе полученного опыта.
- Использовать крупный шрифт, достаточно посмотреть на билборд при запуске шлем, не смотря на то что кто-то скажет, что крупный шрифт не смотрится. Лучше услышать от пользователя что некрасиво выглядит, чем то, что непонятно что написано и он не может с этим работать.
- Все двухмерное меню стараться сдвигать к центру, лучше делать его трехмерным. Но лучше это делать с нуля, а не переделывать имеющееся.
- Использование шлема виртуальной реальности значительно улучшает презентабельность продукта.
- Для использования шлема виртуальной реальности нужна очень хорошо оптимизированная сцена. Иначе при высоких настройках изображение расплывается и организм чувствует себя не совсем хорошо.
- Это написано в мануале, но я от себя добавлю не используйте MSAA 8x, размер RenderTexture для глаза весит >100 Mb в памяти что не может не огорчать.
P.S. Наверно некоторые подумают, что это бред и почему для пространственного курсора я не использовал более дешевый прием с райкастом и рисованием билборда направленного в сторону плеера с вычислением его размера относительно дальности. На сцене в используемом проекте не везде стоят коллайдеры и некоторые коллайдеры не соответствуют размерам объекта и в итоге получается, что курсор иногда висит непонятно где и как.
