Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Шари композиції OpenXR
Вступ
В іграх XR ви зазвичай хочете створити взаємодію користувачів, яка відбувається в 3D-просторі та залучати користувачів доторкатися до об’єктів так, ніби вони торкаються до них у реальному житті.
Однак іноді створення більш традиційного 2D-інтерфейсу неминуче. Однак у XR ви не можете просто додати 2D-компоненти до вашої сцени. Godot потребує інформації про глибину, щоб правильно розташувати ці елементи, щоб вони відображалися в зручному місці для користувача. Навіть з інформацією про глибину є гарнітури з похилими дисплеями, які не дозволяють стандартному 2D-конвеєру правильно відтворювати 2D-елементи.
Рішення полягає в тому, щоб відобразити UI у SubViewport і відобразити результат цього за допомогою ViewportTexture на 3D-сіті. QuadMesh є підходящим варіантом для цього.
Примітка
Перегляньте приклад проекту GUI у 3D для прикладу цього підходу.
Проблема з відображенням вікна перегляду таким чином полягає в тому, що відтворений результат перевіряється на спотворення об’єктива за допомогою середовища виконання XR, і результуюча втрата якості може ускладнити читання тексту інтерфейсу користувача.
OpenXR пропонує вирішення цієї проблеми за допомогою композиційних шарів. Завдяки композиційним шарам вміст вікна перегляду може бути спроектовано на поверхню після спотворення лінзи, що призводить до набагато вищої якості кінцевого результату.
Примітка
Оскільки не всі середовища виконання XR підтримують усі типи шарів композиції, Godot реалізує резервне рішення, де ми візуалізуємо вікно перегляду як частину звичайної сцени, але з вищезазначеними обмеженнями якості.
Попередження
Якщо композиційний рівень підтримується, саме середовище виконання XR представляє підокно перегляду. Це означає, що користувальницький інтерфейс видно лише в гарнітурі, Godot не матиме доступу до нього, і, отже, не відображатиметься, коли на робочому столі є глядач.
Наразі існує 3 вузли, які надають цю функцію:
OpenXRCompositionLayerCylinder показує вміст SubViewport всередині циліндра (або «зрізу» циліндра).
OpenXRCompositionLayerEquirect показує вміст SubViewport всередині сфери (або «зрізу» сфери).
OpenXRCompositionLayerQuad показує вміст SubViewport на плоскому прямокутнику.
Налаштування SubViewport
Першим кроком є додавання SubViewport для нашого двовимірного інтерфейсу, для цього не потрібно виконувати жодних особливих кроків. У нашому прикладі ми позначаємо вікно перегляду як прозоре.
Тепер ви можете створити 2D UI, додавши дочірні вузли до SubViewport, як зазвичай. Бажано зберегти 2D UI у підсцені, це полегшить створення макета.
Попередження
Режим оновлення «Коли видно» не працюватиме, оскільки Godot не може визначити, чи вікно перегляду видиме для користувача. При призначенні нашого вікна перегляду шару композиції Godot автоматично налаштує це.
Додавання композиційного шару
Другим кроком є додавання шару композиції. Ми можемо додати правильний вузол шару композиції як дочірній вузол нашого вузла XROrigin3D. Це дуже важливо, оскільки середовище виконання XR позиціонує все відносно нашого походження.
Ми хочемо розташувати шар композиції так, щоб він знаходився на висоті очей і приблизно на 1-1,5 метри від гравця.
Тепер ми призначаємо SubViewport властивості Layer Viewport і вмикаємо Alpha Blend.
Примітка
Оскільки гравець може відійти від початкової точки, ви захочете змінити положення шару композиції, коли гравець відцентрує перегляд. Використання довідкового простору Місцевий поверх автоматично застосує цю логіку.
Змусити інтерфейс працювати
Поки що ми лише відображаємо наш інтерфейс користувача, щоб він працював, нам потрібно додати код. У цьому прикладі ми зробимо все просто і змусимо один із контролерів працювати як покажчик. Потім ми імітуємо дії миші за допомогою цього вказівника.
Цей код також вимагає додавання вузла MeshInstance3D під назвою Pointer як дочірнього до нашого вузла OpenXRCompositionLayerQuad. Ми налаштовуємо SphereMesh з радіусом 0.01 метра. Ми будемо використовувати це як помічник для візуалізації того, куди вказує користувач.
Основною функцією, яка керує цією функціональністю, є функція intersects_ray на нашому вузлі шару композиції. Ця функція приймає глобальне положення та орієнтацію нашого вказівника та повертає UV, де наш промінь перетинає наше вікно перегляду. Він повертає Vector2(-1.0, -1.0), якщо ми не вказуємо на наше вікно перегляду.
Ми починаємо з налаштування деяких змінних, важливими тут є експортні змінні, які ідентифікують наш вузол контролера, за допомогою якого ми вказуємо на наш екран.
extends OpenXRCompositionLayerQuad
const NO_INTERSECTION = Vector2(-1.0, -1.0)
@export var controller : XRController3D
@export var button_action : String = "trigger_click"
var was_pressed : bool = false
var was_intersect : Vector2 = NO_INTERSECTION
...
Далі ми визначаємо допоміжну функцію, яка приймає значення, повернене з intersects_ray, і дає нам глобальне положення для цієї точки перетину. Ця реалізація працює лише для нашого вузла OpenXRCompositionLayerQuad.
...
func _intersect_to_global_pos(intersect : Vector2) -> Vector3:
if intersect != NO_INTERSECTION:
var local_pos : Vector2 = (intersect - Vector2(0.5, 0.5)) * quad_size
return global_transform * Vector3(local_pos.x, -local_pos.y, 0.0)
else:
return Vector3()
...
Ми також визначаємо допоміжну функцію, яка приймає значення нашого intersect та повертає наше розташування в локальній системі координат області перегляду:
...
func _intersect_to_viewport_pos(intersect : Vector2) -> Vector2i:
if layer_viewport and intersect != NO_INTERSECTION:
var pos : Vector2 = intersect * Vector2(layer_viewport.size)
return Vector2i(pos)
else:
return Vector2i(-1, -1)
...
Основна логіка відбувається в нашій функції _process. Тут ми починаємо з приховування вказівника, потім перевіряємо, чи є у нас дійсний контролер і вікно перегляду, і викликаємо intersects_ray з положенням і орієнтацією нашого контролера:
...
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(_delta):
# Hide our pointer, we'll make it visible if we're interacting with the viewport.
$Pointer.visible = false
if controller and layer_viewport:
var controller_t : Transform3D = controller.global_transform
var intersect : Vector2 = intersects_ray(controller_t.origin, -controller_t.basis.z)
...
Далі ми перевіряємо, чи ми перетинаємося з нашим вікном перегляду. Якщо так, ми перевіряємо, чи наша кнопка натиснута, і розміщуємо покажчик у точці перетину.
...
if intersect != NO_INTERSECTION:
var is_pressed : bool = controller.is_button_pressed(button_action)
# Place our pointer where we're pointing
var pos : Vector3 = _intersect_to_global_pos(intersect)
$Pointer.visible = true
$Pointer.global_position = pos
...
Якщо під час попереднього виклику процесу ми перетиналися, і наш вказівник перемістився, ми готуємо об'єкт InputEventMouseMotion для імітації руху миші та надсилаємо це до області перегляду для подальшої обробки.
...
if was_intersect != NO_INTERSECTION and intersect != was_intersect:
# Pointer moved
var event : InputEventMouseMotion = InputEventMouseMotion.new()
var from : Vector2 = _intersect_to_viewport_pos(was_intersect)
var to : Vector2 = _intersect_to_viewport_pos(intersect)
if was_pressed:
event.button_mask = MOUSE_BUTTON_MASK_LEFT
event.relative = to - from
event.position = to
layer_viewport.push_input(event)
...
Якщо ми щойно відпустили кнопку, ми також готуємо об'єкт InputEventMouseButton для імітації відпускання кнопки та надсилаємо його до нашого вікна перегляду для подальшої обробки.
...
if not is_pressed and was_pressed:
# Button was let go?
var event : InputEventMouseButton = InputEventMouseButton.new()
event.button_index = 1
event.pressed = false
event.position = _intersect_to_viewport_pos(intersect)
layer_viewport.push_input(event)
...
Або, якщо ми щойно натиснули кнопку, ми готуємо об'єкт InputEventMouseButton для імітації натискання кнопки та надсилаємо його до нашого вікна перегляду для подальшої обробки.
...
elif is_pressed and not was_pressed:
# Button was pressed?
var event : InputEventMouseButton = InputEventMouseButton.new()
event.button_index = 1
event.button_mask = MOUSE_BUTTON_MASK_LEFT
event.pressed = true
event.position = _intersect_to_viewport_pos(intersect)
layer_viewport.push_input(event)
...
Далі ми запам’ятовуємо наш стан для наступного кадру.
...
was_pressed = is_pressed
was_intersect = intersect
...
Нарешті, якщо ми не перетинаємося, ми очищаємо наш стан.
...
else:
was_pressed = false
was_intersect = NO_INTERSECTION
Пробивання отворів
Оскільки шар композиції складається поверх результату візуалізації, його можна відобразити перед об’єктами, які фактично знаходяться перед вікном перегляду.
Увімкнувши перфорацію, ви наказуєте Godot відобразити прозорий об’єкт там, де відображається наше вікно перегляду. Він робить це таким чином, що заповнює буфер глибини та очищає поточний результат візуалізації. Все, що знаходиться позаду нашого вікна перегляду, тепер буде очищено, а все, що знаходиться перед нашим вікном перегляду, відображатиметься як зазвичай.
Вам також потрібно встановити Порядок сортування на від’ємне значення, компонувальник XR тепер спочатку намалює вікно перегляду, а потім накладе наш результат візуалізації.
Приклад використання, що показує, як рука користувача неправильно закривається композиційним шаром, коли не використовується перфорація.