凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity でテクスチャを直接 NVENC でエンコードしてみる

はじめに

前回の記事で NVIDIA のハードウェアエンコーダである NVENC(および NVDEC)を使いやすくした NvPipe を Unity で使えるようにしたアセットの紹介をしました。

tips.hecomi.com

しかし問題点として、NvPipe はバッファのポインタしか食わせる口がなく、Unity でテクスチャをエンコードしたい場合は一度 ReadPixels() して GPU からデータをとってきて、それを再度 GPU に与えるという無駄な工程が必要でした。そこで、NvPipe ではなく NVENC を直接使ってエンコードすることで、GPU 上にあるテクスチャを直接利用してエンコードすることを試みてみました。

方針

NvEncoder のサンプルとして video-sdk-samples というサンプルが GitHub に上がっています。

github.com

ただこのリポジトリには、本来 Video Codec SDK のサイトから規約に同意しないとダウンロードできないファイルが含まれていたり、エンコード部分のコードのライセンスの判断が難しいなど、色々と問題があります。また、エンコードのパフォーマンスを最大限引き上げるためのマルチスレッド化の対応も配布されているコードでは(多分)そのままでは利用できません。ただし、GeForce のドライバをインストールすると自動で導入される nvEncodeAPI[64].dll を利用する nvEncodeAPI.h だけは MIT ライセンスで配布されているので、これのみに依存するコードで且つ非同期エンコーディング可能かつシンプルなものを書いてみることにしました。

ダウンロード

github.com

執筆現在ではまだ安定していないので Releases に .unitypackage は上げていないため、試したい方は直接 clone してきてください。

利用方法

Example シーンを見てみますと、Render Camera というゲームオブジェクトがあるのでそれを見てみます。

f:id:hecomi:20190827230509p:plain

uNvEncoder コンポーネントをまず用意します。TextureEncoder コンポーネントでインスペクタから指定された Textureエンコードする設定で uNvEncoder を初期化し、毎フレーム、インスペクタで指定された Texture(ここでは Camera コンポーネントTarget Texture にしていした RenderTextureエンコードします。エンコードが完了すると、バッファのポインタとサイズが onEncoded イベントリスナにやってきます。実際にコードを見てみましょう。

まずはエンコーダのコードです。

...
public class TextureEncoder : MonoBehaviour
{
    public uNvEncoder encoder = null;
    public Texture texture = null;
    public int frameRate = 60;
    public bool forceIdrFrame = true;

    void OnEnable()
    {
        ...
        encoder.StartEncode(texture.width, texture.height, frameRate);
        StartCoroutine(EncodeLoop());
    }

    void OnDisable()
    {
        StopAllCoroutines();
        encoder.StopEncode();
    }

    IEnumerator EncodeLoop()
    {
        for (;;)
        {
            yield return new WaitForEndOfFrame();
            Encode();
        }
    }

    void Encode()
    {
        ...
        encoder.Encode(texture, forceIdrFrame));
        ....
    }
}

StartEncode() で開始、StopEncode() で終了します。毎フレームレンダリング後(WaitForEndOfFrame() のタイミング)で Encode() を呼びます。 Encode() には Texture2D または System.IntPtr(テクスチャのネイティブポインタ、実体は ID3D11Texture2D)を渡すことができます。

では、次に受け取り部分です。

...
public class OutputEncodedDataToFile : MonoBehaviour
{
    ...
    FileStream fileStream_;
    BinaryWriter binaryWriter_;

    void Start()
    {
        fileStream_ = new FileStream(filePath, FileMode.Create, FileAccess.Write);
        binaryWriter_ = new BinaryWriter(fileStream_);
    }

    void OnApplicationQuit()
    {
        fileStream_.Close();
        binaryWriter_.Close();
    }

    public void OnEncoded(System.IntPtr ptr, int size)
    {
        var bytes = new byte[size];
        Marshal.Copy(ptr, bytes, 0, size);
        binaryWriter_.Write(bytes);
    }
}

OnEncoded() でやってきたバッファをファイルに書き出しています。この OnEncoded() はインスペクタ上で uNvEncoder コンポーネントonEncoded に直接セットします。出力された H264 のファイルは VLC プレイヤーなどで再生することが出来ます。

改善点

現状まだエンコード結果がたまにガビガビになるバグがあるのでそこをなんとかしたいです。。また、一つのエンコーダしか作れないので複数の対応も行います。

おわりに

次回は uNvEnc(エンコード)と uNvPipe(デコード)を利用してカラーバッファとデプスバッファを転送するリモートレンダリングをしてみます。