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

Vlastní systém pro interakci s objekty a jejich fyzikální chování

  • Cílem je vytvořit jednoduchý a rozšiřitelný systém pro interakci s objekty, tedy zpracování vstupů, logika za detekcí, sebráním a držením objektu, fyzikální chování držených objektů a jejich možná konfigurace.

Požadavky:

  • Unity 2020.2+
  • Projekt s nainstalovaným a nakonfigurovaným OpenXR Pluginem
  • Dobrá znalost prostředí a chování Unity

Postup

  • Úloha je zpracována po částech

Příprava

  • Práce s fyzikou (PhysX) - navýšení fyzikálních iterací za sekundu na refresh rate headsetu (1/refresh rate) - typicky 90Hz ⇒ 1/90 = 0.0111111

physics

  • Přidaný XR Rig - zdroj pro kameru a reprezentace ovladačů a headsetu v prostoru aplikace
    • Vytváříme vlastní systém, takže u objektů controllerů nebudeme potřebovat XR Interactor, XR Line Visual ani Line Renderer
  • Potřebujeme nějaké objekty se kterými budeme interagovat - ideálně na samostatné vrstvě, aby jsme omezili kolize (o tom ještě později)

items

  • Nejprve vytvoříme skript - HandInteractionManager, který se bude starat o získávání vstupu, detekci a sebrání obkjektů, vizualizaci rukou a držení objektů. (jedná se o příklad - správnější by bylo tyto funkčnosti rozdělit do samostatných skriptů). Každý z kontrolerů bude mít jeden takovýto skript jako svojí komponentu

Zpracování vstupu

  • Primárním cílem XR Pluginu je jednotné rozhraní, které funguje pro všechny komerční headsety. Takže sjednocení základních funkcionalit.
  • Hlavní z nich je input - čtení hodnot tlačítek, joysticku atd.

table

  • Začneme s tvorbou skriptu. Použijeme přímé získání hodnot pomocí InputFeatureUsage a InputDevice
using UnityEngine.XR;

public class HandInteractionManager : MonoBehaviour
{
    public InputDeviceCharacteristics controllerCharacteristics;
    private InputDevice targetDevice;

}
  • Aby jsme mohli ze zařízení získat vstup, potřebujeme získat jeho instanci - to jde několika způsoby, přes tkzv. Charakteristiky, Role nebo XR Node. V tomto příkladu použijeme “Charakteristiky” ke specifikování, který kontroler chceme získat. Ty specifikujeme v Unity Inspectoru, kde atributu přiřadíme např. Left/Right a Controller
void GetControllerDevice()
{
    List<InputDevice> inputDevices = new List<InputDevice>();
    InputDevices.GetDevicesWithCharacteristics(controllerCharacteristics, inputDevices);

    if (inputDevices.Count > 0)
        targetDevice = inputDevices[0];
}
  • Dále samotné funkce pro získání vstupu. Pro interakce s objekty se typicky používá grip button - uživatel musí fyzicky “zmáčknout-zatnout” ruce.
  • CommonUsages.grip operuje v rozmezí [0-1] což se může hodit např. pro animování modelu rukou. Pro samotné uchopení nám ale stačí jen boolean hodnota, kterou budeme větvit logiku kódu (chňapeme/nechňapeme).
bool IsGripHeld(float treshold = 0.9f)
{
    float val = GetGripInputValue();
    if (val >= treshold)
        return true;
    return false;
}

float GetGripInputValue()
{
    if (targetDevice.TryGetFeatureValue(CommonUsages.grip, out float gripValue) && gripValue > 0f)
        return gripValue;
    else
        return 0f;
}
  • Při spuštění získáme údaje o zařízení a v rámci updatu (FixedUpdate protože budeme pracovat s fyzikou pro chování držených objektů) budeme zjišťovat, jestli je grip button aktivní. Při spuštění aplikace ale hned nemusí být kontrolery aktivní nebo se mohou v průběhu vypnout. Musíme tedy ještě kontrolovat, jestli máme validní referenci a případně ji získat znovu.
void Start()
{
    // get xr device
    GetControllerDevice();
}

void FixedUpdate()
{
    if (!targetDevice.isValid) // controller disconnected or not found
    {
        GetControllerDevice();
    }
    else
    {
        if (IsGripHeld())
        {
            // try grabbing object?
        }
        else
        {
            // drop item from hand?
        }
    }
}

Detekce a sebrání objektu

  • Teď když získáváme vstupní údaje o ovladačích, přesuneme se na zjištění validních "interagovatelných" objektů v okolí a jejich "sebrání".
  • Budeme potřebovat několik parametrů:
    • Vrstva, na které se budeme pokoušet objekty skenovat.
    • Vzdálenost od ruky, na kterou budeme objekty detekovat - v tomoto případě štědrých 30cm.
    • Parametry pro mechanizmus na omezení množství pokusů na "sbírání" objektů - debounce.
    • Parametry, kterými budeme referencovat sebraný objekt a jeho vlastnosti.
      • Sebraný objekt bude mít (vytvoříme později) komponentu "IteractableItem", která bude obsahovat informace o chování objektu, jeho umístění atd.
public LayerMask pickupSource;
public float grabDistance = 0.3f;
public float grabDebounce = 0.3f;

GameObject heldItem;
InteractableItem heldItemComponent;
Transform heldItemGripPosition;

bool isHoldingItem = false;
float grabDebounceTimer = 0f;
  • Pro samotné "skenování" okolí využejeme kolizní detekci koulí. (Pro jednoduchost skriptu vybíráme první nalezený objekt)
  • Dále metody pro sebrání a zahození objektu
GameObject GrabScan()
{
    Collider[] colliders = Physics.OverlapSphere(transform.position, grabDistance, pickupSource);
    if (colliders.Length > 0)
        return colliders[0].transform.root.gameObject;
    return null;
}

void Grab(GameObject grabbedItem)
{
    heldItem = grabbedItem;

    // align to hand?

    isHoldingItem = true;
}

void Drop()
{
    heldItem = null;
    isHoldingItem = false;
}
  • Teď můžeme upravit Update o systém omezení frekvence sbírání a o zavolání sebrání nebo zahození v patřičném "stavu". ("Když mačkám tlačítko držení a nic momentálně nedržím, zkusím sebrat nejbližší objekt. Naopak, když tlačítko nedržím a v "ruce" mám objekt, pustím ho")
void FixedUpdate()
{
    if (!targetDevice.isValid)
    {
        GetControllerDevice();
    }
    else
    {
        if (IsGripHeld())
        {
            <----------------------
            if (isHoldingItem)
            {
                // update held item?
            }
            else
            {
                if (grabDebounceTimer <= 0f)
                {
                    GameObject item = TryGrab();
                    if (item)
                    {
                        Grab(item);
                        grabDebounceTimer = grabDebounce;
                    }
                }
                else
                {
                    grabDebounceTimer = -Time.fixedDeltaTime;
                }
            }
            <----------------------
        }
        else
        {
            <----------------------
            if (isHoldingItem)
            {
                Drop();
            }
            <----------------------
        }
    }
}

InteractibleItem - konfigurace interagovatelných objektů

  • Ne každý objekt by se měl při sebrání chovat stejně. Například místo, za které objekt držíme nebude univerzální mezi všemi možnými objkety, stejné platí pro orientaci objketů při sebrání. A nebo další specifické chování a metody držení objektů.
  • Proto vytvoříme skript-komponentu, která bude tyto informace udržovat a při sebrání je tak poskytneme i našemu "systému rukou".
public class InteractableItem : MonoBehaviour
{
    public Transform gripPosition;

    public Vector3 itemHandAlignement = Vector3.zero;

    public bool offsetGrab = false;
}
  • Tento skript bude na všech interagovatelných objektech
  • Co se konfigurace orientace sebraného předmětu, bude následující princip:
    • Objektům ovladačů vytvoříme Empty dítě Hand Alignment - objekt jen s komponentou transform
    • Jeho lokální souřadnice při sebrání upravíme podle paramterů v InteractableItem skriptu
    • Objekt budeme používat jako lokaci, na kterou budeme držený objekt přesouvat
  • Zpět v *HandInteractionManager*u přidáme další parametr, který je referencí na korespondující Hand Alignment objekt.
  • a ještě upravíme Grab a Drop metody
public Transform handAlignmentObject;

void Grab(GameObject grabbedItem)
{
    heldItem = grabbedItem;
    heldItemComponent = grabbedItem.GetComponent<InteractableItem>();

    heldItemGripPosition = heldItemComponent.gripPosition;
    handAlignmentObject.localEulerAngles = heldItemComponent.itemHandAlignement;
    if(heldItemComponent.offsetGrab)
    {
       // todo: offset handAlignemnt local position
       // hint: vector(handPosition<->grabbedItemPosition)
    }

    // align to hand?

    isHoldingItem = true;
}

void Drop()
{
    heldItem = null;
    heldItemComponent = null;
    isHoldingItem = false;
}

Fyzikální chování držených objektů

  • Umíme zíksávat vstup, nacházet blízké objekty, rozlišovat mezi různými druhy chování a víme přesně kde držený objekt chceme mít. Teď potřebujeme zajistit, aby se náš držený objekt choval fyzikálně, takže aby nerušil imerzi procházením zdí a aby interagoval s ostatními objekty podle očekávání.
  • Naštěstí můžeme využít nativních fuností Unity, aby jsme zajistili požadované chování. Využijeme komponent:
    • Rigidbody - Dynamický fyzikální objekt
    • Joint - "kloub, který spojí dva dynamické fyzikální objekty" - v našem případě Configurable joint, který bude fungovat jako 6dof spoj
  • Potřebujeme následující:
    • Samotný InteractibleItem objekt bude dynamický fyzikální objekt - Rigidbody
    • Vytvoříme nový Prefab objekt jakožto "zápěstí" pro držený objekt, který bude Kinematický Rigidbody, což víceméně znamená, že na něj působý pouze síly způsobené "kódem", ale pro ostatní interagující předměty je to normální "fyzikální předmět".
      • Jeho součástí je i Configurable Joint, který v následující konfiguraci postará, aby byl připojený objekt na stejné pozici, ale zároveň ho nijak neomezoval v rotaci

joint1 joint2

  • V kódu HandInteractionMangeru budeme potřebovat pár dalších atributů:

    public GameObject itemWristPrefab;
    
    GameObject heldItemWrist;
    Rigidbody heldItemWristRigidbody;
    ConfigurableJoint wristJoint;
  • Princip bude jednoduchý. Objekt je Rigidbody, takže na něj působí gravitace a ostatní fyzikální objekty. Pokud ho sebereme, vytvoříme novou instanci našeho objektu zápěstí a propojíme ji jako joint connectedBody se sebraným objektem na jeho pozici gripPos. Později pro přesun chyceného objektu budeme hýbat zápěstím, které se bude snažit přemístit držený objekt na jeho místo, ale zároveň bude respektovat fyziku okolního prostředí. Pokuď budeme chtít objekt pustit, stačí jen odstranit objekt zápěstí a samotný puštěný objekt, díky tomu že je Rigidbody, jednoduše upadne nebo odletí(samozdřejmě v závislosti na tom, jak jsem s objektem hýbali před puštěním).

    void Grab(GameObject grabbedItem)
    {
        heldItem = grabbedItem;
        heldItemComponent = grabbedItem.GetComponent<InteractableItem>();
    
        heldItemGripPosition = heldItemComponent.gripPosition;
        handAlignmentObject.localEulerAngles = heldItemComponent.itemHandAlignement;
        if(heldItemComponent.offsetGrab)
        {
           // todo: offset handAlignemnt local position?
           // hint: vector(handPosition<->grabbedItemPosition)
        }
    
        Instantiate(itemWristPrefab, heldItemGripPosition.position, heldItemGripPosition.rotation);
        heldItemWristRigidbody = heldItemWrist.GetComponent<Rigidbody>();
    
        wristJoint = heldItemWrist.GetComponent<ConfigurableJoint>();
        wristJoint.connectedBody = heldItem.GetComponent<Rigidbody>();
    
        isHoldingItem = true;
    }
    
    void Drop()
    {
        wristJoint.connectedBody = null;
        Destroy(heldItemWrist);
    
        heldItemWrist = null;
        heldItemGripPosition = null;
        heldItemWristRigidbody = null;
    
        heldItem = null;
        heldItemComponent = null;
        isHoldingItem = false;
    }

sword

Držení objektu

  • Poslední, co teď zbývá, je synchronizovat pozici "zápěstí" s pozicí ruky.
public void UpdateHeldItem()
{
    UpdateHeldItemPosition();
    UpdateHeldItemRotation();
}
  • Pokuď zároveň držíme objekt a tlačítko grip button, potřebujeme aktualizovat lokaci zápěstí.
void FixedUpdate()
{
    if (!targetDevice.isValid)
        GetControllerDevice();
    else
    {
        if (IsGripHeld())
        {
            if (isHoldingItem)
            {
                UpdateHeldItem();  <----
            }
            else
            {
                --//--
            }
        else
        {
            --//--
        }
    }
}
  • Naše zápěstí je Kinematické Rigidbody a nejlepším způsobem, jak s objektem pohnout je využít Rigidbody metod: MovePosition() a MoveRotation(), které přesunou/orotují objekt na požadovanou pozici/rotaci.
void UpdateHeldItemPosition()
{
    var deltaPos = handAlignmentObject.position - heldItemWrist.transform.position;
    heldItemWristRigidbody.velocity = Vector3.zero;
    heldItemWristRigidbody.MovePosition(heldItemWristRigidbody.position + deltaPos);
}

void UpdateHeldItemRotation()
{
    heldItemWristRigidbody.angularVelocity = Vector3.zero;
    heldItemWristRigidbody.MoveRotation(handAlignmentObject.rotation);
}

Vizualizace rukou

  • Poslední částí tohoto příkladu je vizualní reprezentace rukou. Cheme uživateli ukázat(spíše potvrdit), že jeho ruce nějakém smyslu existují i v této virtuální realitě. Jelikož se ale teď námi držené objekty chovají fyzikálně, znamená to, že nemusí vždy být tam kde se zrovna nachází ovladače. Využijeme jednoduché logiky, aby se ruce tam kde to dává smysl.
  • Budeme potřebovat nějaký model, který bude ruce reprezentovat. (Pro tento příklad jsem použil kouli s průhledným a emisivním materiálem). Dále budeme potřebovat nové atributy pro referenci objektu ruky a pro její "vytvoření". Samozdřejmě objekt ruky chceme vytvořit hned při spuštění.
public GameObject handModelPrefab;
GameObject handObject;

void Start()
{
    // get xr device
    GetControllerDevice();
    // create hands
    handObject = Instantiate(handModelPrefab, transform);  <----
}
  • Logika je dost jednoduchá: Pokud nic nedržíme, pozice rukou je stejná jako pozice ovladače. Pokud ale držíme objekt v ruce, pozice objektu ruky je stejná jako pozice gripPos drženého objektu.
void UpdateHands()
{
    if (isHoldingItem)
    {
        handObject.transform.position = heldItemGripPosition.position;
        handObject.transform.rotation = heldItemGripPosition.rotation;
    }
    else
    {
        handObject.transform.position = transform.position;
        handObject.transform.rotation = transform.rotation;
    }
}
  • A ještě nám zbývá kód volat v každém Updatu, pokud máme validní referenci na ovladač
 void FixedUpdate()
{
    if (!targetDevice.isValid)
    {
        GetControllerDevice();
    }
    else
    {
        --//--

        UpdateHands();
    }

grab

racket

  • Výslekem tohoto skriptu v akci jsou všechny screenshoty z virtuální reality co se v tomto dokumentu vyskytují. Cílem ale nebylo řádek po řádku kód vysvětlit. Naopak, tímto zjednodušeným kódem jsem chtěl spíše poukázat na principy, které za tímto kódem nacházejí.