Лучший сценарий запуска XR
В Настройка XR мы представили стартовый скрипт, который инициализирует нашу настройку, которую мы использовали в качестве скрипта на нашем главном узле. Этот скрипт выполняет минимально необходимые действия для любого интерфейса.
При использовании OpenXR необходимо внести ряд улучшений. Для этого мы создали более сложный стартовый скрипт. Вы найдёте его применение в наших демонстрационных проектах.
В качестве альтернативы, если вы используете XR Tools (см. Представляем инструменты XR), он содержит версию этого скрипта, обновленную некоторыми функциями, связанными с инструментами XR.
Ниже мы подробно опишем скрипт, используемый в наших демонстрациях, и объясним добавленные части.
Сигналы для нашего сценария
Мы вводим в наш скрипт 3 сигнала, чтобы наша игра могла добавлять дополнительную логику:
focus_lostиздается, когда игрок снимает гарнитуру или входит в системное меню гарнитуры.focus_gainedиздается, когда игрок снова надевает гарнитуру или выходит из меню и возвращается в игру.pose_recenteredвыдается, когда гарнитура запрашивает сброс позиции игрока.
Наша игра должна соответствующим образом реагировать на эти сигналы.
extends Node3D
signal focus_lost
signal focus_gained
signal pose_recentered
...
using Godot;
public partial class MyNode3D : Node3D
{
[Signal]
public delegate void FocusLostEventHandler();
[Signal]
public delegate void FocusGainedEventHandler();
[Signal]
public delegate void PoseRecenteredEventHandler();
...
Переменные для нашего скрипта
Мы также вводим в наш скрипт несколько новых переменных:
maximum_refresh_rateбудет управлять частотой обновления гарнитуры, если она поддерживается гарнитурой.xr_interfaceсодержит ссылку на наш интерфейс XR, он уже существовал, но теперь мы вводим его, чтобы получить полный доступ к нашему API XRInterface.xr_is_focussedбудет установлено в значение true всякий раз, когда наша игра имеет фокус.
...
@export var maximum_refresh_rate : int = 90
var xr_interface : OpenXRInterface
var xr_is_focussed = false
...
...
[Export]
public int MaximumRefreshRate { get; set; } = 90;
private OpenXRInterface _xrInterface;
private bool _xrIsFocused;
...
Наша обновленная функция ready
Мы добавляем несколько вещей в функцию ready.
При использовании mobile или forward+ рендера мы устанавливаем vrs_mode области просмотра на VRS_XR. На платформах, которые поддерживают эту функцию, это включит фовеальный рендеринг.
Если мы используем совместимый рендерер, мы проверяем, настроены ли параметры фовеального рендеринга OpenXR, и если нет, выводим предупреждение. Подробнее см. в разделе OpenXR Settings.
Мы подключаем ряд сигналов, которые будут испускаться XRInterface. Мы предоставим более подробную информацию об этих сигналах по мере их реализации.
Мы также завершаем работу приложения, если не удалось успешно инициализировать OpenXR. Теперь это может быть вариантом. Если вы создаёте игру со смешанным режимом, вы настраиваете режим VR при успешном завершении и режим без VR при сбое. Однако при запуске приложения, использующего только VR, на автономной гарнитуре лучше завершить работу при сбое, чем вызвать зависание системы.
...
# Called when the node enters the scene tree for the first time.
func _ready():
xr_interface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.is_initialized():
print("OpenXR instantiated successfully.")
var vp : Viewport = get_viewport()
# Enable XR on our viewport
vp.use_xr = true
# Make sure v-sync is off, v-sync is handled by OpenXR
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
# Enable VRS
if RenderingServer.get_rendering_device():
vp.vrs_mode = Viewport.VRS_XR
elif int(ProjectSettings.get_setting("xr/openxr/foveation_level")) == 0:
push_warning("OpenXR: Recommend setting Foveation level to High in Project Settings")
# Connect the OpenXR events
xr_interface.session_begun.connect(_on_openxr_session_begun)
xr_interface.session_visible.connect(_on_openxr_visible_state)
xr_interface.session_focussed.connect(_on_openxr_focused_state)
xr_interface.session_stopping.connect(_on_openxr_stopping)
xr_interface.pose_recentered.connect(_on_openxr_pose_recentered)
else:
# We couldn't start OpenXR.
print("OpenXR not instantiated!")
get_tree().quit()
...
...
/// <summary>
/// Called when the node enters the scene tree for the first time.
/// </summary>
public override void _Ready()
{
_xrInterface = (OpenXRInterface)XRServer.FindInterface("OpenXR");
if (_xrInterface != null && _xrInterface.IsInitialized())
{
GD.Print("OpenXR instantiated successfully.");
var vp = GetViewport();
// Enable XR on our viewport
vp.UseXR = true;
// Make sure v-sync is off, v-sync is handled by OpenXR
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
// Enable VRS
if (RenderingServer.GetRenderingDevice() != null)
{
vp.VrsMode = Viewport.VrsModeEnum.XR;
}
else if ((int)ProjectSettings.GetSetting("xr/openxr/foveation_level") == 0)
{
GD.PushWarning("OpenXR: Recommend setting Foveation level to High in Project Settings");
}
// Connect the OpenXR events
_xrInterface.SessionBegun += OnOpenXRSessionBegun;
_xrInterface.SessionVisible += OnOpenXRVisibleState;
_xrInterface.SessionFocussed += OnOpenXRFocusedState;
_xrInterface.SessionStopping += OnOpenXRStopping;
_xrInterface.PoseRecentered += OnOpenXRPoseRecentered;
}
else
{
// We couldn't start OpenXR.
GD.Print("OpenXR not instantiated!");
GetTree().Quit();
}
}
...
Начало сеанса
Этот сигнал OpenXR испускает при настройке сеанса. Это означает, что гарнитура завершила все необходимые настройки и готова к приёму контента. Только в этот момент различная информация становится доступной.
Главное, что мы делаем здесь, — это проверяем частоту обновления нашей гарнитуры. Мы также проверяем доступные частоты обновления, сообщаемые средой выполнения XR, чтобы определить, нужно ли установить более высокую частоту обновления для нашей гарнитуры.
Наконец, мы подбираем частоту обновления физики под частоту обновления гарнитуры. Godot по умолчанию работает с частотой обновления физики 60 кадров в секунду, в то время как гарнитуры работают минимум с 72 кадрами в секунду, а современные гарнитуры часто достигают 144 кадров в секунду. Несоответствие частоты обновления физики приведёт к подтормаживанию, поскольку кадры рендерятся без движения объектов.
...
# Handle OpenXR session ready
func _on_openxr_session_begun() -> void:
# Get the reported refresh rate
var current_refresh_rate = xr_interface.get_display_refresh_rate()
if current_refresh_rate > 0:
print("OpenXR: Refresh rate reported as ", str(current_refresh_rate))
else:
print("OpenXR: No refresh rate given by XR runtime")
# See if we have a better refresh rate available
var new_rate = current_refresh_rate
var available_rates : Array = xr_interface.get_available_display_refresh_rates()
if available_rates.size() == 0:
print("OpenXR: Target does not support refresh rate extension")
elif available_rates.size() == 1:
# Only one available, so use it
new_rate = available_rates[0]
else:
for rate in available_rates:
if rate > new_rate and rate <= maximum_refresh_rate:
new_rate = rate
# Did we find a better rate?
if current_refresh_rate != new_rate:
print("OpenXR: Setting refresh rate to ", str(new_rate))
xr_interface.set_display_refresh_rate(new_rate)
current_refresh_rate = new_rate
# Now match our physics rate
Engine.physics_ticks_per_second = current_refresh_rate
...
...
/// <summary>
/// Handle OpenXR session ready
/// </summary>
private void OnOpenXRSessionBegun()
{
// Get the reported refresh rate
var currentRefreshRate = _xrInterface.DisplayRefreshRate;
GD.Print(currentRefreshRate > 0.0F
? $"OpenXR: Refresh rate reported as {currentRefreshRate}"
: "OpenXR: No refresh rate given by XR runtime");
// See if we have a better refresh rate available
var newRate = currentRefreshRate;
var availableRates = _xrInterface.GetAvailableDisplayRefreshRates();
if (availableRates.Count == 0)
{
GD.Print("OpenXR: Target does not support refresh rate extension");
}
else if (availableRates.Count == 1)
{
// Only one available, so use it
newRate = (float)availableRates[0];
}
else
{
GD.Print("OpenXR: Available refresh rates: ", availableRates);
foreach (float rate in availableRates)
{
if (rate > newRate && rate <= MaximumRefreshRate)
{
newRate = rate;
}
}
}
// Did we find a better rate?
if (currentRefreshRate != newRate)
{
GD.Print($"OpenXR: Setting refresh rate to {newRate}");
_xrInterface.DisplayRefreshRate = newRate;
currentRefreshRate = newRate;
}
// Now match our physics rate
Engine.PhysicsTicksPerSecond = (int)currentRefreshRate;
}
...
В видимом состоянии
Этот сигнал испускается OpenXR, когда игра становится видимой, но не находится в фокусе. Это немного странное описание в OpenXR, но по сути это означает, что игра только что запущена и мы собираемся перейти в состояние фокуса, что пользователь открыл системное меню или только что снял гарнитуру.
Получив этот сигнал, мы обновим наше сфокусированное состояние, изменим режим процесса нашего узла на отключенный, что приостановит обработку этого узла и его дочерних элементов, и выдадим наш сигнал focus_lost.
Если вы добавили этот скрипт в корневой узел, игра будет автоматически останавливаться при необходимости. Если вы этого не сделали, вы можете подключить к сигналу метод, который выполняет дополнительные изменения.
Примечание
Пока ваша игра находится в видимом состоянии, поскольку пользователь открыл системное меню, Godot продолжит рендеринг кадров, а отслеживание положения головы останется активным, так что игра будет видна в фоновом режиме. Однако отслеживание контроллера и рук будет отключено до тех пор, пока пользователь не выйдет из системного меню.
...
# Handle OpenXR visible state
func _on_openxr_visible_state() -> void:
# We always pass this state at startup,
# but the second time we get this it means our player took off their headset
if xr_is_focussed:
print("OpenXR lost focus")
xr_is_focussed = false
# pause our game
get_tree().paused = true
emit_signal("focus_lost")
...
...
/// <summary>
/// Handle OpenXR visible state
/// </summary>
private void OnOpenXRVisibleState()
{
// We always pass this state at startup,
// but the second time we get this it means our player took off their headset
if (_xrIsFocused)
{
GD.Print("OpenXR lost focus");
_xrIsFocused = false;
// Pause our game
GetTree().Paused = true;
EmitSignal(SignalName.FocusLost);
}
}
...
В сосредоточенном состоянии
Этот сигнал выдаётся OpenXR, когда игра получает фокус. Это происходит по завершении запуска, но он также может выдаваться, когда пользователь выходит из системного меню или снова надевает гарнитуру.
Также обратите внимание, что если игра запускается, а пользователь не надел гарнитуру, игра остается в 'видимом' состоянии до тех пор, пока пользователь не наденет гарнитуру.
Предупреждение
Поэтому важно ставить игру на паузу в режиме видимости. Если этого не сделать, игра продолжит работать, пока пользователь не взаимодействует с ней. Кроме того, когда игра возвращается в режим фокусировки, всё отслеживание контроллера и рук внезапно включается снова, что может привести к сбоям в игре, если вы не отреагируете соответствующим образом. Обязательно протестируйте это поведение в своей игре!
При обработке нашего сигнала мы обновим состояние фокуса, возобновим работу нашего узла и отправим сигнал focus_gained.
...
# Handle OpenXR focused state
func _on_openxr_focused_state() -> void:
print("OpenXR gained focus")
xr_is_focussed = true
# unpause our game
get_tree().paused = false
emit_signal("focus_gained")
...
...
/// <summary>
/// Handle OpenXR focused state
/// </summary>
private void OnOpenXRFocusedState()
{
GD.Print("OpenXR gained focus");
_xrIsFocused = true;
// Un-pause our game
GetTree().Paused = false;
EmitSignal(SignalName.FocusGained);
}
...
В состоянии остановки
Этот сигнал выдаётся OpenXR при переходе в состояние остановки. Время появления этого сигнала может различаться на разных платформах. На некоторых платформах он выдаётся только при закрытии игры. На других платформах он также выдаётся каждый раз, когда игрок снимает гарнитуру.
На данный момент этот метод является лишь временным.
...
# Handle OpenXR stopping state
func _on_openxr_stopping() -> void:
# Our session is being stopped.
print("OpenXR is stopping")
...
...
/// <summary>
/// Handle OpenXR stopping state
/// </summary>
private void OnOpenXRStopping()
{
// Our session is being stopped.
GD.Print("OpenXR is stopping");
}
...
В центрированной позе
Этот сигнал отправляется OpenXR, когда пользователь запрашивает центрирование своего взгляда. По сути, это сообщает вашей игре, что пользователь теперь смотрит вперёд, и вам следует переориентировать игрока так, чтобы он смотрел вперёд в виртуальном мире.
Поскольку это зависит от вашей игры, ваша игра должна реагировать соответствующим образом.
Всё, что мы делаем здесь, — это посылаем сигнал pose_recentered. Вы можете подключиться к этому сигналу и реализовать код рецентрирования. Часто достаточно вызвать center_on_hmd().
...
# Handle OpenXR pose recentered signal
func _on_openxr_pose_recentered() -> void:
# User recentered view, we have to react to this by recentering the view.
# This is game implementation dependent.
emit_signal("pose_recentered")
...
/// <summary>
/// Handle OpenXR pose recentered signal
/// </summary>
private void OnOpenXRPoseRecentered()
{
// User recentered view, we have to react to this by recentering the view.
// This is game implementation dependent.
EmitSignal(SignalName.PoseRecentered);
}
}
На этом наш скрипт готов. Он был написан так, чтобы его можно было использовать повторно в нескольких проектах. Просто добавьте его в качестве скрипта на основной узел (и при необходимости расширьте) или добавьте на дочерний узел, предназначенный специально для этого скрипта.