Parte: 13 — VR, AR y experiencias inmersivas · Fuente: Godot Docs — Setting up XR y XR nodes ⏱️ Duración estimada: 75 min · Nivel: Avanzado
Con OpenXR ya habilitado, toca montar la escena mínima que todo proyecto VR necesita. La jerarquía es siempre la misma: un XROrigin3D que representa el espacio físico de juego, un XRCamera3D hija que sigue la cabeza del jugador, y uno o dos XRController3D que siguen los mandos. Inicializas OpenXR, activas use_xr en el viewport y de repente estás dentro de la escena, con las manos moviéndose en el aire.
En esta clase construyes esa primera escena VR completa desde cero, con mallas visibles para las manos y un script que arranca OpenXR de forma robusta. Aprenderás por qué el XROrigin3D es tu "punto cero" físico, cómo la cámara hereda el tracking sin que tú muevas nada, y cómo asignar cada controller a la mano correcta con tracker. Al final lo verás en el visor (o en el simulador) con las dos manos siguiéndote.
Al finalizar, el alumno podrá:
tracker.use_xr desde un script robusto.| # | Tema | Por qué importa |
|---|---|---|
| 1 | XROrigin3D | Es el punto cero del espacio físico del jugador. |
| 2 | XRCamera3D | Sigue la cabeza sin código; hija del origin. |
| 3 | XRController3D | Sigue cada mando; base de toda interacción. |
| 4 | Propiedad tracker |
Asocia un controller a left_hand/right_hand. |
| 5 | Inicialización robusta | Evitar arrancar sin runtime o sin use_xr. |
| 6 | Mallas de manos | Feedback visual: el jugador ve dónde están sus manos. |
| 7 | Escala de mundo | 1 unidad = 1 metro; la escala importa en VR. |
| 8 | Simulador XR | Probar sin visor durante el desarrollo. |
button_pressed/button_released y lee input_float.tracker: propiedad que identifica qué mano representa el controller (left_hand, right_hand). Clave: sin asignarla, el mando no se posiciona.button_pressed(name): señal del XRController3D al pulsar un botón mapeado. Clave: name es la acción del Action Map, no el botón físico.input_float(name): valor analógico de una acción (gatillo 0.0–1.0). Clave: útil para gatillos y grips graduales.Parte del proyecto con OpenXR habilitado de la clase 230. Necesitas Godot 4.x y, para ver el resultado inmersivo, un visor con runtime OpenXR o el simulador que incluye el addon godot-xr-tools (https://github.com/GodotVR/godot-xr-tools). El addon es opcional para esta clase (montaremos todo con nodos nativos), pero su simulador es cómodo para probar sin hardware.
Consulta la referencia de nodos XR de Godot (https://docs.godotengine.org/en/stable/classes/class_xrorigin3d.html y los nodos relacionados). Ten preparadas dos mallas simples para las manos: sirven BoxMesh o SphereMesh pequeñas mientras no tengas modelos de manos. Trabaja siempre pensando en metros: una mesa está a ~0,75 m del suelo.
Montarás la primera escena VR completa con manos visibles e inicialización de OpenXR.
Crea una escena nueva con raíz Node3D llamada MundoVR. Añade un WorldEnvironment y una DirectionalLight3D para ver algo, y un StaticBody3D con un MeshInstance3D (un plano) como suelo a y = 0.
Añade como hijo un XROrigin3D (XROrigin). Dentro de él añade un XRCamera3D (XRCamera). No la muevas: el tracking la posicionará.
Añade dos XRController3D como hijos del XROrigin: LeftHand y RightHand. En el Inspector, fija la propiedad Tracker: left_hand para el primero y right_hand para el segundo.
A cada controller añádele un MeshInstance3D hijo con una BoxMesh de ~0,08 m para ver la posición de cada mano.
Adjunta este script a MundoVR para inicializar OpenXR y reaccionar a las entradas:
extends Node3D
@onready var izquierda: XRController3D = $XROrigin/LeftHand
@onready var derecha: XRController3D = $XROrigin/RightHand
var xr_interface: XRInterface
func _ready() -> void:
xr_interface = XRServer.find_interface("OpenXR")
if xr_interface and xr_interface.initialize():
get_viewport().use_xr = true
print("VR lista. Vistas: ", xr_interface.get_view_count())
else:
push_warning("Sin runtime OpenXR: se verá en modo plano para depurar.")
# Conectamos las señales de botones de ambas manos.
derecha.button_pressed.connect(_on_boton.bind("derecha"))
izquierda.button_pressed.connect(_on_boton.bind("izquierda"))
func _on_boton(nombre_accion: String, mano: String) -> void:
print("Botón '%s' en mano %s" % [nombre_accion, mano])
# Pulso háptico corto como confirmación (frecuencia, amplitud, duración, retardo).
var controller: XRController3D = derecha if mano == "derecha" else izquierda
controller.trigger_haptic_pulse("haptic", 0.0, 0.5, 0.1, 0.0)
func _process(_delta: float) -> void:
# Ejemplo: leer el gatillo derecho como valor 0.0–1.0.
var gatillo: float = derecha.get_float("trigger")
if gatillo > 0.5:
pass # Aquí iría la acción de "disparo" o similar.
Asegúrate de que las acciones trigger y un botón (por ejemplo el gatillo o ax_button) existen en el Action Map (clase 230) y están enlazadas al perfil de tu mando.
Ejecuta con F5. Con visor y runtime activo verás la escena en estéreo, la cámara seguirá tu cabeza y las dos cajas seguirán tus manos. Pulsa un botón y observa en consola el mensaje y el pulso háptico en el mando.
Sin visor, la advertencia aparecerá y verás la escena en modo plano; útil para comprobar que la jerarquía y las conexiones no dan error.
Ya tienes la escena base de VR sobre la que se construyen la locomoción y la interacción de las clases siguientes.
SphereMesh y ajusta su tamaño a algo natural.button_released) se imprima otro mensaje.get_float("grab") y colorea la malla de la mano cuando supere 0,5.Construye una escena VR con XROrigin3D, XRCamera3D y dos XRController3D correctamente asignados a left_hand y right_hand, cada uno con una malla visible, e inicializa OpenXR activando use_xr. Al pulsar un botón, la mano correspondiente debe emitir un pulso háptico y registrar el evento en consola.
Criterio de aceptación: en visor o simulador, la cámara sigue la cabeza y ambas mallas siguen las manos; al pulsar el botón mapeado, la consola imprime la mano y la acción, y el mando vibra; sin runtime, aparece la advertencia y la escena carga en modo plano sin errores.
| Síntoma | Causa y arreglo |
|---|---|
| Las manos no se ven ni se mueven | Falta asignar tracker (left_hand/right_hand) al XRController3D. |
| La cámara no sigue la cabeza | XRCamera3D no es hija del XROrigin3D. Corrige la jerarquía. |
| Todo se ve gigante o diminuto | Escala incorrecta. Modela en metros: 1 unidad = 1 m. |
button_pressed nunca dispara |
La acción no está en el Action Map o sin binding. Revísala. |
| El mando no vibra | Nombre de háptico erróneo. Usa la acción háptica definida (p. ej. "haptic"). |
❓ ¿Por qué no debo mover la XRCamera3D por código? Porque su transform lo controla el tracking de la cabeza en cada frame. Si la mueves, el runtime la sobrescribe o provocas conflicto y mareo. Para mover al jugador, mueve el XROrigin3D.
❓ ¿Puedo tener un solo controller? Sí, para experiencias de una mano. Basta con un XRController3D asignado a la mano que uses; deja el otro fuera.
❓ ¿Qué diferencia hay entre get_float y is_button_pressed? get_float devuelve el valor analógico de una acción (gatillo 0,0–1,0); is_button_pressed devuelve un booleano para acciones de tipo botón.
❓ ¿Necesito modelos de manos realistas? No para empezar. Cajas o esferas bastan para validar el tracking; luego puedes sustituirlas por mallas de manos o los assets de godot-xr-tools.
Clase 230 - OpenXR y el estándar abierto