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

Malování pomocí štětce ve VR

Úvod

V tomto tutoriálu společně vytvoříme ve virtuální realitě nástroj Štětec, pomocí kterého bude možné v prostoru malovat. Pro ovládání VR hardware použijeme XR Interaction Toolkit, který je součástí Unity jako balíček.

Prerekvizity

  • Unity Engine 3D verze 2019.4
  • XR Interaction Toolkit balíček - Stáhne se skrze Unity přes Window → Package Manager → XR Interaction Toolkit. Pokud ho v seznamu balíčků nemůžete najít, klikněte na Advanced → Show preview packages. Balíček nainstalujte kliknutím na tlačítko Install.
  • Po instalaci balíčku zkontrolujte, že potřebné SDKs jsou také nainstalované. Bežte do Edit → Project Settings → Player a úplně dole klikněte na Virtual Reality Supported. Níže by se po chvíli měli objevit SDK pro Oculus a OpenVR, které je i pro HTC Vive headset.
1

Připravení scény a XR Rigu

Do scény umístíme jednoduchou plochu, která bude sloužit jako zem pomocí kliknutí pravého tlačítka do projektové hierarchie a 3D Object → Plane a následně vytvoříme také XR → Device-based → Room-Scale XR Rig, což je objekt, který bude řídit a sledovat náš VR hardware. Tento objekt je právě součástí XR Interaction Toolkitu, pokud ho v nabídce nemáte, ujistěte se, že jste balíček nainstalovali.

Komponenta XR Rig se skládá z hlavní kamery a levé a pravé ruky s předem připnutými skripty. Pro účely tohoto návodu nebude potřeba skriptů XR Ray Interactor a Line Visual, které jsou připnuté na obou rukách. Můžeme je tedy bez problému smazat. Na každém kontroleru tedy ve výsledku zůstane pouze základní skript XR Controller. Pro interakce a sebrání štetce naopak ještě jeden skript z XR Interaction toolkitu přidáme a tím je XR Direct Interactor. Tomuto skriptu nemusíme nastavovat žádné specifické hodnoty. V neposlední řadě přídáme na obě ruce Sphere Collider, kterému nastavíme Radius na 0.2 a zaškrtneme ho jako Is Trigger.

2

Obě ruce mají ve skriptu místo na model, který je bude ve VR reprezentovat. Momentálně pokud spustíme hru, naše controllery totiž neuvidíme. Vytvořme v projektové hierarchii prázdný objekt s názvem Hand a přetáhněme pod něj model ovladače pro HTC Vive, který si můžete stáhnout společně s texturami níže. V projektu si vytvořte složku Models, Textures, Materials a Prefabs pro lepší přehlednost.

  • HTC Vive controller model ke stažení ZDE
  • Textura ke stažení ZDE

Jak zmiňuji výše, stažený model ovladače vložíme jako dítě pod prazdný objekt Hand a pootočíme ho okolo osy Y o 180 stupňů, aby měl správnou orientaci. Vytvoříme nový materiál pomocí kliku pravého tlačítka, přiřadíme staženou texturu do Albedo políčka a přetáhnutím nastavíme materiál na model ovladače. Celý objekt Hand poté vezmeme a přetáhneme do složky Prefabs, uděláme z něj tím instanciovatelný objekt, sloužící jako šablona.

3

Levé i pravé ruce v XR Rigu nastavíme Model Prefab právě tuto námi vytvořenou šablonu. Poté můžeme ze scény objekt Hand smazat, stačí, že žije ve složce Prefabs.

0

Model štětce

Pro usnadnění práce jsem pro vás připravil model štětce společně s texturama, které jsou stažené z odsud. Níže uvedené soubory umístěte do patřičných složek.

Po stažení a importu výše uvedených assetů vytvoříme 3 nové materiály. Brush_bottom materiálu namapujeme stažené textury dřeva a přetahneme ho na spodní část modelu štětce. Brush_top bude sestávat ze stažených textur pro ocel a přetáhneme ho na násadu v horní části. Brush_head materiál přetáhneme na úplnou špičku štětce, využijeme ho až později, až budeme potřebovat dynamicky změnit jeho barvu. Jeho barvu prozatím necháme defaultní bílou.

4

Uchopení štetce

Předtím než budeme se štětcem kreslit, nejdříve ho uděláme uchopitelným. Na kořenový objekt tak připneme skript XR Grab Interactable (tento skript je také součástí XR Interaction Toolkitu). Všimněme si, že se automaticky na objekt přidalo i Rigidbody komponenta, která udává jak na předmět působí ve virtuálním světě fyzika. Vzhledem k tomu, že nechceme, aby nám štětec pořád padal na zem, zaklikněme Is Kinematic checkbox a odškrtněme Use Gravity.

V inspektoru přidáme objektu nový komponent a tím bude Box Collider, který slouží XR Grab Interactable skriptu k detekci kolize s naším ovladačem. Tlačítkem Edit Collider nastavíme přímo ve scéně odpovídající velikost tak, aby kopírovala štětec. Je důležité, tento nově vytvořený collider ručně přetáhnout do proměnné Colliders v XR Grab Interactable skriptu (znázorněno na obrázku). Níže ve skriptu si můžeme všimnout rozbalovacího panelu, který skrývá interaktivní eventy, na které můžeme nějak reagovat a volat jiné funkce. Tuto funkcionalitu za chvíli použijeme.

5

Zkuste teď zapnout hru a měli byste být schopni uchopit štětec do ruky. Předtím nezapomeňte korektně naškálovat štětec, aby nebyl moc veliký nebo malý a zároveň byl v dosahu XR Rigu. Nemáme implementován totiž pohyb ve VR. Na ovladači stáčí zmáčknout boční Grip tlačítko a držet ho. Štětec tak uchopíme do ruky. Je však špatně napozicován, což spravíme vytvořením takzvaného pivota (prázdný GameObject umístěn jako dítě pod hlavní objekt štětce), kterého libovolně natočíme a jeho Transform přetáhneme do Attach Transform proměnné v XR Grab Interactable skriptu. V mém případě jsem m udal rotaci X: -90, Y: 0, Z: -45

6

Kreslící skript

XR Rig, model štětce a interakci uchopení už máme připravenou. Teď už jen stačí implementovat funkcionalitu kreslení. Začněme tím, že vytvoříme nový skript Draw.cs, který připneme na kořenový objekt štětce. Jeho první proměnnou bude pozice, odkud se má začít kreslit. Dále pak šířka čáry, její barva a parametrizovatelná vzdálenost mezi jednotlivými body křivky. Ostatní privátní proměnné slouží k uchovávání aktuálně kreslené čáry pomocí komponenty Line Renderer.

Ukázka 1. Draw.cs
public class Draw : MonoBehaviour
{
    public Transform drawPositionSource;
    public float lineWidth = 0.03f;
    public Material lineMaterial;
    public float distanceThreshold = 0.05f;

    private bool isDrawing;
    private LineRenderer currentLine;
    private List<Vector3> currentLinePositions = new List<Vector3>();
}

Pomocí dvou Public metod pak budeme moci začít a přerušit vlastní kreslení. Tyto metody budou volány z XR Grab Interactable skriptu po vyvolání příslušných Eventů. Tyto metody se jmenují StartDrawing a StopDrawing a vypadají nějak takto.

Ukázka 2. Draw.cs
public void StartDrawing()
{
    isDrawing = true;

    // Create new line
    GameObject line = new GameObject("Line");
    currentLine = line.AddComponent<LineRenderer>();

    UpdateLine();
}

public void StopDrawing()
{
    isDrawing = false;
    currentLinePositions.Clear();
    currentLine = null;
}

Na závěr máme metodu UpdateDrawing, která kontroluje vzdálenost mezi hrotem štětce a naposledy vytvořeným bodem a pokud je větší jak zadaná vzdálenost, zavolá druhou privátní metodu UpdateLine, která přidá křivce další bod a vykreslí mezi nimi čáru. Zároveň nastaví čáře danou šířku a barvu v podobě materiálu. Samozřejmostí je, že metoda UpdateDrawing se musí volat každý snímek, protože uživatel bude se štětcem pořád hýbat. Celá podoba skriptu je tedy zde.

Ukázka 3. Draw.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;

public class Draw : MonoBehaviour
{
    public Transform drawPositionSource;
    public float lineWidth = 0.03f;
    public Material lineMaterial;
    public float distanceThreshold = 0.05f;

    private bool isDrawing;
    private LineRenderer currentLine;
    private List<Vector3> currentLinePositions = new List<Vector3>();

    // Update is called once per frame
    void Update()
    {
        if(isDrawing)
        {
            UpdateDrawing();
        }
    }

    public void StartDrawing()
    {
        isDrawing = true;

        // Create new line
        GameObject line = new GameObject("Line");
        currentLine = line.AddComponent<LineRenderer>();

        UpdateLine();
    }
    public void StopDrawing()
    {
        isDrawing = false;
        currentLinePositions.Clear();
        currentLine = null;
    }

    private  void UpdateDrawing()
    {
        if(!currentLine || currentLinePositions.Count == 0)
        {
            return;
        }
        Vector3 lastSetPosition = currentLinePositions[currentLinePositions.Count - 1];
        if(Vector3.Distance(lastSetPosition, drawPositionSource.position) > distanceThreshold)
        {
            UpdateLine();
        }
    }

    private void UpdateLine()
    {
        // Add created starting line to list of lines
        currentLinePositions.Add(drawPositionSource.position);
        currentLine.positionCount = currentLinePositions.Count;
        currentLine.SetPositions(currentLinePositions.ToArray());

        // Update the visual of line
        currentLine.startWidth = lineWidth;
        currentLine.material = lineMaterial;
    }
}

Po návratu do Editoru je nutné přetáhnout a inicializovat parametry dříve vytvořeného skriptu. Vytvořme prázdný objekt tip, který bude reprezentovat hrot štětce a umístěme ho tam. Následně ho přetáhneme do Draw skriptu do kolonky Draw Position Source. Parametr Line Width doporučuji nastavit na 0.005, Distance Threshold na 0.01 a Line Material můžeme nastavit jako Default-Line (už přítomný v Assets od Unity). S těmito hodnotami si samozřejmě můžete hrát, záleží na tom, čeho chcete dosáhnout.

Posledním krokem je nastavení již zmiňovaných XR Grab Interactable eventů. V kolonce On Activate přidáme nový listener a přetáhneme do něj připnutý Draw skript. Vpravo najdeme veřejnou metodu StartDrawing. To samé provedeme s eventem On Deactivate, zde akorát přiřadíme metodu StopDrawing. Po stistknutí Trigger tlačítka na ovladači se tak zavolají námi vytvořené metody v Draw skriptu. Výsledné nastavení vypadá takto.

7

Můžete spustit scénu, uchopit štětec a začít v prostoru kreslit. Vlastní čára se skláda z bodů, mezi kterými se vykresluje křivka. Tento shluk bodů existuje ve scéně jako všechny ostatní GameObjecty.

Závěr

V tomto krátkém tutoriálu jsme společně zvládnuli importovat model štětce, připravit XR Rig za pomoci XR Interaction Toolkitu a implementovat funkcionalitu kreslení pomocí Draw.cs skriptu. Zkuste jako domácí úkol přidat možnost výběru barvy. Výběr barvy můžete namapovat na touchpad, kde si zjistíte X a Y souřadnici doteku palce a po konverzi těchto souřadnic dostanete barvu v HSV prostoru. Dynamicky pak dokážete změnit materiál barvy

Ukázka 4. Hint pro převod vstupu z touchpadu na barvu (X a Y koordináty na barvu)
private void ChangeBrushColor()
{
    // Calculate and normalize angle from X & Y coordinates from the controller
    float angle = Mathf.Atan2(touchpadInput.x, touchpadInput.y) * Mathf.Rad2Deg;
    float normalAngle = angle - 90;
    if(normalAngle < 0)
    {
        normalAngle = 360 + normalAngle;
    }
    // Convert to radians and calculate max X and Y coords
    float rads = normalAngle * Mathf.PI / 180;
    float maxX = Mathf.Cos(rads);
    float maxY = Mathf.Sin(rads);

    // Calculate percentage between current X, Y coords and max
    float percentageX = Mathf.Abs(touchpadInput.x / maxX);
    float percentageY = Mathf.Abs(touchpadInput.y / maxY);

    // Prepare color in HSV space
    float hue = normalAngle / 360.0f;
    float saturation = (percentageX + percentageY) / 2;
    float value = 1f;

    // Set paintbrush tip to this new color
    Color color = Color.HSVToRGB(hue, saturation, value);
    paintBrushHead.GetComponent<Renderer>().material.color = color;
    currentLineColor = color;
}