Rive Unityを使ってみる

# はじめに

本記事は[QualiArts Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qualiarts)の9日目の記事になります。
本日はRiveというツールと、Unityで再生するシンプルの実装について紹介します。

# Riveの概要

https://rive.app/

公式ページには様々な実用例が載っていますが、UIやグラフィックのアニメーションとインタラクションを作るソフトウェアになります。

### Editor

タイムラインやステートマシンを使える高機能の編集ツール。
画像にメッシュとボーンを付けてアニメーションする事ができます。
Vector Graphicsもあって、解像度に依存しない絵を作る事も可能です。

![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532685/7995d50f-674b-70b3-1f98-f8737d1dae38.jpeg)

### Format

.rivという独自のファイルフォーマット。
書き出す際、不要の情報を取り除いて必要なアセットをバイナリにパッキングするコンパクトの方式です。

### Runtime

様々なRuntimeが開発されています。
[Unity Runtime](https://rive.app/blog/rive-for-unity-now-available)は2023年11月頃に追加されて、D3D12とVulkan以外は利用可能です。
*D3D12とVulkanは今後対応予定

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532685/4ce317dc-832b-d577-9bc5-45504c07fd93.png">

### Community Files

ユーザーが作成したファイルを公開し、他人も簡単にリミックスできます。
他人が作成した物のタイムラインやステートマシンを参考できるので、ツールの上達には役立つ仕組みと思いました。

https://rive.app/community/files/


# Unityの導入

現時点はPackageManager経由でgitのリンクを追加するか、ローカルにcloneして追加できます。

https://github.com/rive-app/rive-unity

今回は[Bobbeh](https://rive.app/@Bobbeh/)さんが作成したこちらのファイルを使って実演します。

https://rive.app/community/files/5000-10112-sith-de-mayo/

# 描画の概要

ライブラリーの内部はC++で実装されており、各プラットフォームの描画Apiを使って描画しています。
そのため、Unityに表示するためには事前にRenderTextureを作成してライブラリーに渡す必要があります。

### アセットを用意する

まず、.rivファイルをUnityに入れれば自動的に認識されてインポート処理が走ります。
正しくインポートされたら、アセットのインスペクタービューから見た目をプレビューできます。

<img width="300px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532685/b18319b6-4204-cb66-ba82-c57aa1020818.png">

### ファイルをロードする

アセットをロードして、ArtboardとStateMachineを取得します。
Artboardはキャンパスのような概念であり、今回のサンプルファイルには一つしかないのでindex:0として取得します。
StateMachineはこのArtboardの内部状態を管理するストーとマシンです。

```C#
using Rive;

public Asset asset;
private Artboard _artboard;
private StateMachine _stateMachine;

void LoadFile()
{
    var file = File.Load(asset); 
    _artboard = file.Artboard(0);
    _stateMachine = _artboard?.StateMachine();
}
```

### 描画対象を準備する

公式のサンプルコードはカメラにCommandBufferを追加してそのまま描画しますが、本記事はCanvasのRawImageに描画する事を試しています。
Rive.TextureHelperにあるDescriptorメソッドを使ってRenderTextureを生成します。

```C#
public RawImage rawImage;

void PrepareRenderTarget()
{
    var size = rawImage.rectTransform.rect.size;
    _renderTexture = new RenderTexture(TextureHelper.Descriptor((int)size.x, (int)size.y));
    rawImage.texture = _renderTexture;

    // 自前にRenderTextureを使う際はuv反転を行う必要があります
    if (SystemInfo.graphicsUVStartsAtTop)
    {
        rawImage.rectTransform.localScale = new Vector3(1f, -1f, 1f);
    }
}
```

### RiveのRendererを初期化する

作成したRenderTextureを使ってRenderQueueとRendererを作成します。
このRendererは描画関連の命令を内部ライブラリーに転送してくれる存在です。

```C#

private RenderQueue _renderQueue;
private Rive.Renderer _riveRenderer;
    
void InitializeRenderer()
{
    _renderQueue = new RenderQueue(_renderTexture, false);
    _riveRenderer = _renderQueue.Renderer();
}
```

### Artboardを設定する

Align()はArtboardのレイアウト周りを設定できます。個別設定の意味は[こちら](https://help.rive.app/runtimes/layout)を参考してください。

```C#
void SetArtboard()
{
    _riveRenderer.Align(Fit.cover, Alignment.Center, _artboard);
    _riveRenderer.Draw(_artboard);
}
```

### ループ処理

描画は毎フレーム行う必要があって、Renderer.Submit()を実行することで描画のCommandBufferを発行します。
ステートマシンがある場合は、内部の時間を進ませるためにAdvance(Time.deltaTime)を実行します。

```C#
void Update()
{   
    _stateMachine?.Advance(Time.deltaTime);
    _riveRenderer.Submit();
}
```

この時点、プレイモードに入ればアニメーションが動いてあるのを確認できました。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532685/feeb73ca-3afe-dcbe-8ab2-a79661437be0.gif">

### クリック処理

今回のサンプルファイルはクリックする処理が入っているため、クリック処理も実装してみましょう。
Rive空間はUnityと違って(0, 0)の初期位置が左上にあるため、y位置の反転が必要になります。
下の例はPointerMove、PointerDown、PointerUp、3種類の処理をステートマシンに転送するイメージです。

```C#
private bool _wasMouseDown;
private Vector3 _lastMousePoint;

void Update()
{
    ProcessPointer(); // ここに追加
    
    _stateMachine?.Advance(Time.deltaTime);
    _riveRenderer.Submit();
}
    
void ProcessPointer()
{
    var camera = Camera.main;
    var mousePoint = camera.ScreenToViewportPoint(Input.mousePosition);
    var riveScreenPoint = new Vector2(
        mousePoint.x * camera.pixelWidth,
        (1 - mousePoint.y) * camera.pixelHeight
    );

    if (_lastMousePoint != mousePoint)
    {
        _stateMachine?.PointerMove(GetRiveLocalPoint(riveScreenPoint, camera.pixelWidth, camera.pixelHeight));
        _lastMousePoint = mousePoint;
    }
    
    if (Input.GetMouseButtonDown(0))
    {
        _stateMachine?.PointerDown(GetRiveLocalPoint(riveScreenPoint, camera.pixelWidth, camera.pixelHeight));
        _wasMouseDown = true;
    }
    else if (_wasMouseDown)
    {
        _wasMouseDown = false;
        _stateMachine?.PointerUp(GetRiveLocalPoint(riveScreenPoint, camera.pixelWidth, camera.pixelHeight));
    }
}

Vector2 GetRiveLocalPoint(Vector2 riveScreenPoint, int width, int height)
{
    return _artboard.LocalCoordinate(
        riveScreenPoint,
        new Rect(0, 0, width, height),
        Fit.cover,
        Alignment.Center
    );
}
```

これでクリックしたら内部のアンメーションが切り替わる事を確認できました。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532685/8f62d66d-36db-cbec-c303-fa7d969bb85f.gif">


# 音声の再生

Riveは画像以外に音声も使えますので、音声の再生もチャレンジしてみます。
次は[pedroalpera](https://rive.app/@pedroalpera/)さんのピアノを使って実演します。

https://rive.app/community/files/9759-19603-piano-with-sound/

まず、適切の音声設定からAudioEngineを作成します。
その後、対象Artboardに設定します。

```C#
void BindAudio()
{
    var channelCount = 1;
    switch (AudioSettings.speakerMode)
    {
        case AudioSpeakerMode.Mono:
            channelCount = 1;
            break;
        case AudioSpeakerMode.Stereo:
            channelCount = 2;
            break;
        case AudioSpeakerMode.Quad:
            channelCount = 4;
            break;
        case AudioSpeakerMode.Surround:
            channelCount = 5;
            break;
        case AudioSpeakerMode.Mode5point1:
            channelCount = 6;
            break;
        case AudioSpeakerMode.Mode7point1:
            channelCount = 8;
            break;
        case AudioSpeakerMode.Prologic:
            channelCount = 2;
            break;
    }

    _audioEngine = AudioEngine.Make(channelCount, AudioSettings.outputSampleRate);
    _artboard.SetAudioEngine(_audioEngine);
}
```

他の方法もありますが、今回はMonoBehaviourのOnAudioFilterReadを通して、UnityのAudioSourceに音声の波形データを流して鳴かします。

```C#
void OnAudioFilterRead(float[] data, int channels)
{
    if (_audioEngine == null) return;
    _audioEngine.Sum(data, channels);
}
```

実際キーをクリックして音声を再生する事が確認できました。

<img width="400px" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532685/ada5c585-6894-6b18-9823-f3a58cec6ccc.gif">

# まとめ

これでRiveファイルをUnityで再生するシンプルの実装ができました。
Riveにはまだ他の機能がいっぱいありますので、これをきっかけに少しでもRiveに興味を持ってもらえれば幸いです。

ちなみに[公式のサンプルコード](https://github.com/rive-app/rive-unity-examples)には他の機能もありますので、ぜひ参考してみてください。[ライブラリー側の描画Api](https://github.com/rive-app/rive-unity-examples/blob/67bbd634f3768cdf34b7f03c41001897cede2660/getting-started/Assets/Rive/RiveProceduralRendering.cs#L84-L88)も公開していますので、[UnityのVector Graphics](https://docs.unity3d.com/Packages/com.unity.vectorgraphics@2.0/manual/index.html)の代わりに使えるかもしれません。

以上、9日目の記事でした。引き続き[今後のカレンダー投稿](https://qiita.com/advent-calendar/2024/qualiarts)を宜しくお願いします。

※本記事のソースコード
https://gist.github.com/thammin/086b4990a786aabe08d0629f3a6202dc