BI-PGA Programování grafických aplikací
Jdi na navigaci předmětu

Unity XR Interaction Toolkit - Interakce pomocí XRBaseInteractable a XRBaseInteractor

Demo

Projekt se scénou je možné najít v repozitáři. Celé skripty lze najít zde.

Demo obsahuje jednoduché/mírně pokročilé příklady pro interakci s objekty a rozděluje se na tři hlavní části:

  1. Skládání kola
  2. Otevírání dveří, točení stolu rukojetí
  3. Střílení zbraní na terč

Co tato dokumentace obsahuje

  • Jednoduchý přehled pro interakci s rukama a locomotion pohyb
  • Interakce pomocí tříd XRBaseInteractable a XRBaseInteractor
  • Pohyb objektů pomocí komponenty Rigidbody
  • Vytvoření shaderu pomocí ShaderGraph

Co tato dokumentace neobsahuje

  • Inicializace balíčku Unity XR Interaction Toolkit a virtuální reality v projektu
  • Interakce pomocí laserových paprsků a teleportační pohyb

Projekt

Projekt používá Unity 2019.4.11. Při tvorbě nového projektu je nutné vytvořit projekt se šablonou Universal Render Pipeline.

source

Balíčky

Projekt obsahuje následující balíčky (Window > Package Manager)

  • Unity XR Interaction Toolkit 1.0.0 (verze z března 2021)
  • OpenVR Desktop
  • Shader Graph

Scéna

Scéna výsledného projektu obsahuje objekty připravené pro interakci ve virtuální realitě.

source

Základ

  • Vstupy jsou získány pomocí akcí (Action-based systém), byl použit základní set akcí.
  • Ve scéně je umístěn základní prefab kamery Room-Scale XR Rig (Action-based).
  • Interakce na ovladačích je direktní pomocí komponenty XRDirectInteractor (přímá interakce, ruka).
  • Pohyb je uskutečněn pomocí komponenty Continuous Move Provider (Action-based).
  • Otáčení je uskutečněno pomocí komponenty Continuous Turn Provider (Action-based).

Tvorba nových tříd

  • XRBaseInteractable a XRBaseInteractor jsou třídy, které komunikují přes XRInteractionManager.
  • Z těchto tříd dědí ostatní třídy a každá z nich se liší svojí funkcionalitou, třídy obsažené v balíčku jsou:

source source

Důležité:

Proměnné v inspektoru Aby bylo možné zobrazit nové (přidané) parametry (proměnné) v inspektoru tříd dědících z XRBaseInteractor nebo XRBaseInteractable, je nutné vytvořit novou složku Editor a v ní skripty pro každý takový skript. Skript umožní zobrazení proměnných v inspektoru. Struktura skriptu je následující:

using UnityEditor.XR.Interaction.Toolkit;
using UnityEditor;

// Example for XRGrabInteractable
[CustomEditor(typeof(CustomClass))]
public class CustomClassEditor :  XRGrabInteractableEditor
{
    private SerializedProperty variable = null;

    protected override void OnEnable()
    {
        base.OnEnable();
        variable = serializedObject.FindProperty("variable");
    }
    protected override void DrawProperties()
    {
        base.DrawProperties();
        EditorGUILayout.PropertyField(variable);
    }
}

Skládání kola

Skládačka obsahuje siluetu kola, která se postupně skládá z částí kola. Silueta kola barevně napovídá, zda lze část napojit na dané místo. Kolo obsahuje 3 různé typy částí. Po složení se kolo transformuje do složené podoby a lze ho uchopit.

source source source source source

Část

Část je charakterizována skriptem Part, který dědí ze třídy XRGrabInteractable. Pomocí této komponenty lze objekt uchopit pomocí akce Selekce. Některé části obsahují vlastní transformaci AttachTransform, která posune a natočí objekt při uchopení automaticky na tuto transformaci. Skript obsahuje typ části (enum třída PartType).

public enum PartType
{
    Circle,
    Cube,
    Root
}
public class Part : XRGrabInteractable
{
    public PartType type;
}

source

Balík částí

Část kola lze získat z nekonečného balíku částí. Každá část má svůj balík a je vizualizován stejným modelem. Tento objekt obsahuje skript Spawner, který dědí ze třídy XRBaseInteractable. Třída XRBaseInteractable umožní interagovat s objektem pomocí akce Selekce. Tato třída neslouží k uchopení předmětů, pouze k vyvolání jednoduché interakce. Po interakci s tímto objektem se vytvoří a automaticky uchopí nová instance objektu části.

source

Metoda OnEnable() je vyvolána v moment, kdy je objekt aktivní. Pokud objekt není deaktivován ve scéně tak je tato událost vyvolána ihned po načtení scény. Event selectEntered je vyvolán při selekci tohoto objektu. Této události je přidán listener SelectObject.

protected override void OnEnable()
{
    base.OnEnable();
    // Event to be called when this object is selected
    selectEntered.AddListener(SelectObject);
}

Metoda SelectObject() musí mít parametry typu SelectEnterEventArgs, protože slouží jako listener při selekci objektu. Argumenty args obsahují interactor a interactable, který byly částí interakce (interactor - ruka, interactable - objekt). Jelikož části jsou charakteru XRGrabInteractable a tím jsou uchopitelné, tak je možné jednoduše použít metodu ForceSelect, kde se část uchopí do interactoru (ruky) hned po vytvoření. Proměnná interactionManager je dostupná v každé třídě XRBaseInteractable a XRBaseInteractor.

private void SelectObject(SelectEnterEventArgs args)
{
    // Instantiate
    GameObject prefabObject = Instantiate(prefab, interactorTransform.position, interactorTransform.rotation);
    XRGrabInteractable grab = null;

    // Force select the new object into the interacting interactor
    if(prefabObject.TryGetComponent<XRGrabInteractable>(out grab))
        interactionManager.ForceSelect(args.interactor, grab);
}

Socket

Silueta kola se skládá z několika částí se skriptem Socket, který dědí ze třídy XRSocketInteractor. Tento interactor umožní přichytit objekt na danou transformaci (pozici a rotaci) automaticky po upuštění v určité oblasti. Objekt musí obsahovat komponentu Collider (jakýkoliv) charakteru Trigger, jinak nebude XRSocketInteractor fungovat správně. Transformace lze určit v inspektoru interactoru parametrem Attach Transform.

Skript Socket určuje a omezuje, zda objekt lze přichytit to socketu. Toto se hodí při skládání kola, neboť omezuje části a jejich napojení. Toto je možné pomocí přetížené metody CanSelect() (podobně funguje metoda CanHover()). Metoda vrací bool (true - interactable lze napojit na interactor, false - nelze). Metoda Connected kontroluje, zda typy v socketu a typ části (která by se měla napojit) se shodují.

public override bool CanSelect(XRBaseInteractable interactable)
{
    return base.CanSelect(interactable) && Connected(interactable);
}
public override bool CanHover(XRBaseInteractable interactable)
{
    return base.CanHover(interactable) && Connected(interactable);
}

Další částí je změna materiálu. Materiál socketu se změní při přiblížení (hover) a deaktivuje se uplně při selekci. Jednoduchý skript SocketMaterial obsahuje metody, které mění/vypnou/zapnou první materiál komponenty MeshRenderer v objektu.

public void MaterialDefault()
{
    meshRenderer.material = materialDefault;
}
public void MaterialHover()
{
    meshRenderer.material = materialHover;
}
public void MaterialOff()
{
    meshRenderer.enabled = false;
}
public void MaterialOn()
{
    meshRenderer.enabled = true;
}

Příkladem je přetížená metoda OnHoverEntered() ve tříde Socket, která se vyvolá při přiblížení objektu ke socketu.

protected override void OnHoverEntered(HoverEnterEventArgs args)
{
    base.OnHoverEntered(args);
    socketMaterial.MaterialHover();
}

source

Silueta kola

Silueta kola obsahuje skript SocketManager, který v metodě Update kontroluje zda jsou všechny sockety správně napojené a při úspěchu zničí všechny objekty částí a vytvoří instanci složeného objektu. Kolo obsahuje jednoduchý skript XRGrabInteractable, aby ho bylo možné uchopit.

private void Update()
{
    List<XRBaseInteractable> interactables = new List<XRBaseInteractable>();

    // Check sockets
    foreach(Socket socket in sockets)
    {
        if(!socket.IsConnected)
            return;
        // The selected interactable in the socket - part of the wheel
        interactables.Add(socket.selectTarget);
    }
    // If all are connected correctly, destroys all parts (interactables list) and instantiates new object
    Connect(interactables);
}

source

Materiál

Materiály kola (default, hover) jsou vytvořeny pomocí vlastního shaderu vytvořeného nástrojem Shader Graph (CreateShaderPBR Shader).

Výsledný shader lze použít jako šablonu pro tvorbu materiálů pomocí zadaných parametrů (Power a Color). Modrý materiál je defaultní, zelený pro přiblížení (hover) správné části.

source

Otevírání dveří, točení stolu rukojetí

Interaktivní a pohybující prostředí je umožněno pomocí komponent Rigidbody a XRGrabInteractable. V tomto projektu je demonstrace těchto interakcí pomocí otevíracích dveří a točení kulatého stolu se siluetou kola rukojetí.

source source

Dveře

Polička obsahuje dveře, které lze otevřít pomocí jednoduché kliky. Objekt dveří je rodičem objektu kliky. Dveře (ne klika) obsahují skript Handle, který dědí ze třídy XRGrabInteractable. Důležité je nastavit parametr Movement Type na Velocity Tracking. Toto umožní pohyb objektu pomocí komponenty Rigidbody a její kinematiky. Dále je třeba nastavit parametr Collider na collider kliky. Toto umožní uchytit dveře pouze za kliku.

source

Samostatný skript mění typ tlačítka v XRDirectInteractor. Původní je nastaven na mód Toggle (první stisknutí - selekce, druhé stisknutí - deselekce). Metoda OnSelectEntered() změní mód na State (držení tlačítka - selekce, upuštění - deselekce). Metoda OnSelectExited() vrátí mód na původní nastavení.

protected override void OnSelectEntered(SelectEnterEventArgs args)
{
    base.OnSelectEntered(args);

    // If the interactor is a hand, change the InpuTriggerType for this interaction
    if(args.interactor is XRDirectInteractor hand)
        hand.selectActionTrigger = XRBaseControllerInteractor.InputTriggerType.State;
}

Správné otáčení je umožněno pomocí komponenty Hinge Joint, která je připnutá na dveřích. Parametr Anchor nastaví bod, kolem kterého se objekt otáčí (střed rotace). Parametr Axis určuje osu, kolem které se objekt otáčí (rotační osa). Parametr Limits určuje limity pro úhel otáčení. Limity lze nastavit přímo ve scéně.

source

Stůl

Stůl je otáčen rukojetí, objekty jsou na sobě nezávislé (v rámci hiearchie, rozdíl oproti dveří s klikou). Stůl obsahuje komponentu Hinge Joint (podobně jako dveře) a bod otáčení je nastaven na střed kulatého stolu.

Rukojeť obsahuje stejný skript jako klika dveří (Handle). Jediný rozdíl dělá parametr Collider, který není třeba nastavovat (automaticky se vybere collider samostatného objektu). Aby otáčení rukojetí bylo napojeno na otáčení stolu, je nutné přidat komponentu Fixed Joint. Tato komponenta spojuje pohyb objektů (nezávislých) pomocí Rigidbody. Proto v parametru Connected Body je napojen stůl a jeho komponenta Rigidbody.

Střílení zbraní na terč

Zbraň je uchopitelná a při její aktivaci je možné z ní vystřelit. Při kolizi střely s terčem je vyvolána daná metoda a tím terč reaguje na střelu.

source source source

Zbraň

Zbraň obsahuje skript Gun, který dědí ze třídy XRGrabInteractable. Atribut Attach Transform je nastaven na vlastní transformaci, která umožní správně uchopit zbraň do ruky. Parametry obsahují prefab kulky a transformaci kulky (kde se má nová kulka instancovat po vystřelení).

source

Při vyvolání akce Aktivace je vystřelena kulka ze zbraně. Toto obstarává událost activated, do které je přidán listener Shoot.

protected override void OnEnable()
{
    // Activate button event
    activated.AddListener(Shoot);
    base.OnEnable();
}

Metoda Shoot() má parametry typu ActivateEventArgs, neboť je vyvolána při aktivaci. Metoda vytvoří instanci kulky v místě před zbraní (určeno parametrem transformace). Z objektu se získá třída Bullet, která vyvolá metodu Fire(). Z argumentů args je získán interactor (XRDirectInteractor) a na daném ovladači jsou vyvolány haptické odezvy.

private void Shoot(ActivateEventArgs args)
{
    GameObject bulletObject = Instantiate(bulletPrefab, bulletTransform.position, bulletTransform.rotation);
    Bullet bullet = null;

    // Fire the bullet
    if(bulletObject.TryGetComponent<Bullet>(out bullet))
        bullet.Fire(bulletTransform.forward);

    // Haptics feature on interactor
    if(args.interactor is XRDirectInteractor hand)
        hand.xrController.SendHapticImpulse(1.0f, 0.2f);
}

Kulka

Kulka obsahuje skript Bullet. Parametrem je rychlost kulky, která určuje v jaké rychlosti se bude pohybovat.

source

Metoda Fire() přidá komponentně Rigidbody sílu pomocí Rigidbody.AddForce. Síla je dána směrem (forward) a velikostí (speed). Po určité době je objekt zničen.

public void Fire(Vector3 forward)
{
    // Apply force to rigidbody so the bullet starts moving in appropriate direction
    rigidbody.AddForce(forward * speed);

    // Destroy after 5 seconds
    Destroy(gameObject, 5.0f);
}

Při kolizi objektu kulky s jiným objektem je vyvolána daná metoda jako reakce na kulku. Kolizi kontroluje událost OnCollisionEnter(), která je vyvolána automaticky při kolizi kulky s jakýmkoliv colliderem. Metoda zjistí, zda objekt kolize implementuje rozhraní IHittable. Je vhodné implementovat rozhraní, protože objekty mohou mít vlastní reakce (metody) na kulku (např. zničení, pohyb aj.). Pokud objekt obsahuje IHittable, tak je vyvolána jeho metoda Hit, která se liší pro každý implementovaný objekt. V tomto případě je implementováno toto rozhraní v terči.

private void OnCollisionEnter(Collision other)
{
    Gun gun = null;

    // Ignore collision with gun
    if(other.gameObject.TryGetComponent<Gun>(out gun))
        return;

    IHittable hittable = null;

    // The object of the collider implements the interface IHittable
    if(other.transform.TryGetComponent<IHittable>(out hittable))
        hittable.Hit(transform, rigidbody.velocity.magnitude);

    // Destroy after 0.1 seconds
    Destroy(gameObject, 0.1f);
}

Terč

Střed terče obsahuje skript Target. Tato třída implementuje rozhraní IHittable a jeho metodu Hit().

source

Metoda je velice jednoduchá a pouze aplikuje sílu na Rigidbody pomocí Rigidbody.AddForce (podobně jako u kulky). Směr síly je dán směrem pohybující se kulky a síla je dána velikostí této rychlosti.

public interface IHittable
{
    void Hit(Transform hitTransform, float force);
}
public void Hit(Transform hitTransform, float force)
{
    if(rigidbody)
        rigidbody.AddForce(hitTransform.forward * force);
}

Tyč terče obsahuje skript Handle a komponentu Hinge Joint (podobně jako u stolu s rukojetí), která má limitní úhel otáčení a charakter pružiny. Charakter pružiny zajišťuje a simuluje pružinu při aplikování síly na terč. Limitní úhel je proto nastaven na obě strany. Parametr Connected Body je nastaven na kořen tyče (nezávislý objekt). Střed terče obsahuje komponentu Fixed Joint, která se napojuje na tyč terče (Connected Body). Pomocí této konstrukce lze hýbat s terčem pomocí tyče. Proto tyč obsahuje skript Handle, aby ji bylo možné uchytit a hýbat s ní a terčem.

source