e.blog

主にUnity/UE周りのことについてまとめていきます

ネイティブテクスチャ経由で画面キャプチャ(RenderTexture)を保存する

概要

今回はiOSのネイティブプラグインを利用してRenderTextureをそのままファイル保存するプラグインを作ったので、作る過程で得られた知見やハマりどころなどをメモしていきたいと思います。

RenderTextureにはTexture2Dが持っているEncodeToPNGEncodeToJPGメソッドがなく、さらにそもそもEncodeToPNGなどは非常に重い処理となっています。

なのでRenderTextureをそのままネイティブ側に渡してそれが保存できないかな、と思ったのが実装するに至った経緯です。

実際に実行したときの動画↓

今回の実装にあたり、ふじきさんには多大なご助力をいただきました。本当にありがとうございます。

ふじきさんが作られた動画キャプチャツールの投稿を見たのがきっかけで質問させていただき、色々ご教授いただきました。
そのツールはこちら↓

フロー

まずは全体の流れを把握するためにフローを概観してみます。

  1. RenderTextureを用意
  2. CommandBufferを利用して画面をキャプチャ
  3. Texture2DReadPixelsを利用してRenderTextureピクセル情報を読み出す(*1)
  4. RenderTextureGetNativeTexturePtrを使ってネイティブテクスチャのポインタを取得
  5. (4)のポインタをネイティブプラグインへ送信
  6. (5)のテクスチャを適切な形に変換する
  7. (6)のデータをUIImageにして保存

*1 ... ReadPixelsを行わないと(おそらく)GPUにデータがすぐにアップロードされず、画像保存に失敗します。もしかしたらGL.IssuePluginEventを利用するとうまくいくかもしれません。 色々調査した結果、1フレーム遅延させることでReadPixelsを使わなくても正常に保存することができました。

以上が大まかな処理の流れとなります。

今回実装したものはGitHubに上げてあるので動作を見たい方はそちらをご覧ください。

github.com

RenderTextureを用意

ここはむずかしいところはありません。
ファイルとして用意してもいいですし、ランタイムで用意してもよいです。
今回はランタイムで以下のように、Start時に用意するようにしています。

_buffer = new RenderTexture(Screen.width, Screen.height, 0);
_buffer.Create();

CommandBufferを用意する

コマンドバッファは画面をキャプチャする用途で利用しています。
セットアップは以下のように行っています。

_commandBuffer = new CommandBuffer();
_commandBuffer.name = "CaptureScreen";

_buffer = new RenderTexture(Screen.width, Screen.height, 0);
_buffer.Create();

_commandBuffer.Blit(BuiltinRenderTextureType.CurrentActive, _buffer);

// スクショを撮るタイミングでカメラにコマンドバッファをアタッチ
Camera.main.AddCommandBuffer(CameraEvent.BeforeImageEffects, _commandBuffer);

コマンドバッファはシンプルに、現在のアクティブなレンダーバッファの内容をレンダーテクスチャにコピーしているだけです。
コマンドバッファをアタッチするタイミングはスクショを撮るタイミングです(スクショ撮影用メソッドを呼ぶタイミング)。

CommandBufferについては凹みさんがこちらの記事でとても詳しく解説されています。

tips.hecomi.com

ReadPixelsでピクセル情報を読み出す(※ 必要ありませんでした)

キャプチャを行ったらTexture2DReadPixelsピクセル情報を読み出します。
保存だけを行いたい場合はこれは不要な処理となりますが、GPUへのデータアップロードの関連なのか、これを行わないとネイティブ側の保存時に空白の画像が保存されてしまい、うまくいきませんでした。

フローのところでも書きましたが、もしかしたらGL.IssuePluginEventを利用して読み出すことでうまく動くかもしれません。(これについては後日調査します)

CommandBufferでキャプチャ後、その後すぐに保存するのではなく、1フレーム遅延させることで正常に保存できることを確認しました。

コード的には以下のようにしています。(ReadPixelsしていたコードを変更しています)

    private IEnumerator SaveTexture()
    {
        yield return _waitForEndOfFrame;

        Debug.Log("Save texture to the file.");

        Camera.main.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, _commandBuffer);

        _image.texture = _buffer;

        // To save the RenderTexture as file needs to wait one frame.
        yield return _waitForEndOfFrame;

        Debug.Log("Will show the texture.");

        _SaveTextureImpl(_buffer.GetNativeTexturePtr(), gameObject.name, nameof(CallbackFromSaver));
    }

RenderTextureのGetNativeTexturePtrを使ってネイティブテクスチャのポインタを取得

ここがひとつめの重要な点です。
UnityのTexture2DRenderTextureにはGetNativeTexturePtrというメソッドがあり、ネイティブ側のテクスチャのポインタを取得する方法があります。
今回のプラグインではネイティブ側で、この生成済みのテクスチャのポインタを利用して処理を行います。

取得して送信している箇所を抜粋すると以下のようになります。

// 第2、第3引数はネイティブ側からコールバックを受け取るためのもの
_SaveTextureImpl(_buffer.GetNativeTexturePtr(), gameObject.name, nameof(CallbackFromSaver));

GetNativeTexturePtrのドキュメントは以下です。

docs.unity3d.com

ドキュメントから抜粋すると以下のように書かれています。

On Direct3D-like devices this returns a pointer to the base texture type (IDirect3DBaseTexture9 on D3D9, ID3D11Resource on D3D11, ID3D12Resource on D3D12). On OpenGL-like devices the GL texture "name" is returned; cast the pointer to integer type to get it. On Metal, the id pointer is returned. On platforms that do not support native code plugins, this function always returns NULL.

要するにこれはプラットフォームによって返ってくる意味が違っているということです。(プラットフォームごとに適切なテクスチャへのポインタが返ってくる)
そして今回はiOS向けの話なのでid<MTLTexture>へのポインタが返ってくることが分かります。

On Metal, the id pointer is returned.

ネイティブプラグインへテクスチャのポインタを送る

そしてこちらが重要な点のふたつめです。
前段で取得したネイティブテクスチャのポインタをネイティブプラグイン側へ送り受け取る必要があるわけですが、適切にキャストして利用する必要があります。

これを間違えるとクラッシュしたり、ということがあるので気をつけてください。
ポインタを取得してキャストするコードは以下のようになります。

extern "C" void _SaveTextureImpl(unsigned char* mtlTexture, const char* objectName, const char* methodName)
{
    id<MTLTexture> tex = (__bridge id<MTLTexture>)(void*)mtlTexture;

    // 後略
}

まず、テクスチャのポインタはunsigned char*型で受け取ります。
そしてそれを(__bridge id<MTLTexture>)(void*)mtlTexture;のように、いったんvoid*型を経由してから最後にid<MTLTexture>型にキャストします。

加えて注意点として、C側からObjective-C側へのキャストには(__bridge)を利用してキャストを行う必要があります。

このブリッジにはいくつか種類があり、ARC管理下にいれるか、など状況に応じて使い分ける必要があります。

__bridgeを利用したキャストについては以下の記事を参考にしてみてください。

balunsoftware.jp

また今回はUnity側で生成したテクスチャなのでGC対象にもなっています。
そのため、ネイティブ側で管理対象にはせず、そのままキャストするだけに留めます。

これを、ARC管理対象などにしてしまうとBAD ACCESSでアプリがクラッシュするので注意が必要です。
(ここらへんまだ詳しくないのであれですが、万全を期すなら、Unity側でGC対象のポインタをロック(GCHandle.Alloc)してメモリ位置が変更されないようにするなどのケアは必要かもしれません。が、今回はとにかく保存するところまでを書くのでこのあたりには触れません)

テクスチャを適切な形に変換する

テクスチャのポインタを受け取り、テクスチャの情報にアクセスすることができるようになりましたが、このままだと各ピクセルのデータの並びが異なっているため色味が反転したような絵になって保存されてしまいます。

これに対して、適切に対処してくれるコードを公開してくれていた記事があったのでこちらを参考にさせていただきました。

qiita.com

具体的に何をしているかというと、取得したMTLTextureはRGBAではなくBGRAの並びになっているので、それをRGBAな形に並び替える処理をしています。


上下の反転について

また参考にした記事では、環境によっては上下が反転してしまうため、それを補う処理も同時に施されています。

該当のコードは以下のようになっています。

// flipping image vertically
let flippedBytes = bgraBytes // share the buffer
var flippedBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: flippedBytes),
            height: vImagePixelCount(self.height), width: vImagePixelCount(self.width), rowBytes: rowBytes)
vImageVerticalReflect_ARGB8888(&rgbaBuffer, &flippedBuffer, 0)

ただ今回のサンプルでは上下の反転は必要なかったのでコメントアウトしてあります。
上下の反転があったら上のコードを試してみてください。


この工程を経ることで無事、望んだ形のデータが手に入ります。

上の記事ではMTLTextureの拡張として書かれているのでこれを少しだけ改変してコンバータクラスとして実装しました。

実際に利用しているコードは以下です。

//
//    MTLTexture+Z.swift
//    ZKit
//
//    The MIT License (MIT)
//
//    Copyright (c) 2016 Electricwoods LLC, Kaz Yoshikawa.
//
//    Permission is hereby granted, free of charge, to any person obtaining a copy
//    of this software and associated documentation files (the "Software"), to deal
//    in the Software without restriction, including without limitation the rights
//    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//    copies of the Software, and to permit persons to whom the Software is
//    furnished to do so, subject to the following conditions:
//
//    The above copyright notice and this permission notice shall be included in
//    all copies or substantial portions of the Software.
//
//    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//    WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//    THE SOFTWARE.
//

import Foundation
import CoreGraphics
import MetalKit
import GLKit
import Accelerate

class MTLTextureConverter : NSObject {
    
    @objc static func convert(texture: MTLTexture) -> UIImage?
    {
        
        assert(texture.pixelFormat == .bgra8Unorm)
        
        // read texture as byte array
        let rowBytes = texture.width * 4
        let length = rowBytes * texture.height
        let bgraBytes = [UInt8](repeating: 0, count: length)
        let region = MTLRegionMake2D(0, 0, texture.width, texture.height)
        texture.getBytes(UnsafeMutableRawPointer(mutating: bgraBytes), bytesPerRow: rowBytes, from: region, mipmapLevel: 0)
        
        // use Accelerate framework to convert from BGRA to RGBA
        var bgraBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: bgraBytes),
                                       height: vImagePixelCount(texture.height), width: vImagePixelCount(texture.width), rowBytes: rowBytes)
        
        let rgbaBytes = [UInt8](repeating: 0, count: length)
        var rgbaBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: rgbaBytes),
                                       height: vImagePixelCount(texture.height), width: vImagePixelCount(texture.width), rowBytes: rowBytes)
        let map: [UInt8] = [2, 1, 0, 3]
        vImagePermuteChannels_ARGB8888(&bgraBuffer, &rgbaBuffer, map, 0)
        
        // flipping image virtically
        // let flippedBytes = bgraBytes // share the buffer
        // var flippedBuffer = vImage_Buffer(data: UnsafeMutableRawPointer(mutating: flippedBytes),
        //                                   height: vImagePixelCount(texture.height), width: vImagePixelCount(texture.width), rowBytes: rowBytes)
        // vImageVerticalReflect_ARGB8888(&rgbaBuffer, &flippedBuffer, 0)
        
        // create CGImage with RGBA
        let colorScape = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
        guard let data = CFDataCreate(nil, rgbaBytes, length) else { return nil }
        guard let dataProvider = CGDataProvider(data: data) else { return nil }
        let cgImage = CGImage(width: texture.width, height: texture.height, bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: rowBytes,
                              space: colorScape, bitmapInfo: bitmapInfo, provider: dataProvider,
                              decode: nil, shouldInterpolate: true, intent: .defaultIntent)
        
        return UIImage(cgImage: cgImage!)
    }
}

UIImageとして保存する

前段までで無事にUnityからテクスチャを受け取ることができました。あとはこれをUIImageとしてファイルに保存すれば終了です。

シンプルに保存するだけでいいのであれば以下のように1行で書くことができます。

UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil);

なお、保存後にコールバックを受け取りたい場合は適切にセットアップして、コールバックを受け取るオブジェクトを生成する必要があります。
具体的には以下のシグネチャを持つオブジェクトを生成し、セレクタを渡してやることで実現できます。

期待されるシグネチャ

- (void)image:(UIImage *)image
    didFinishSavingWithError:(NSError *)error
                 contextInfo:(void *)contextInfo;

これを実装したオブジェクトを作ると以下のようになります。
(Unityへのコールバックをする部分も一緒に実装した例です)

#import "CaptureCallback.h"
 
@implementation CaptureCallback

- (id)initWithObjectName:(NSString *)_objectName
              methodName:(NSString *)_methodName;
{
    if (self = [super init])
    {
        self.objectName = _objectName;
        self.methodName = _methodName;
    }
    return self;
}
 
- (void)savingImageIsFinished:(UIImage *)_image didFinishSavingWithError:(NSError *)_error contextInfo:(void *)_contextInfo
{
    const char *objectName = [self.objectName UTF8String];
    const char *methodName = [self.methodName UTF8String];

    if (_error != nil)
    {
        NSLog(@"Error occurred with %@", _error.description);
        UnitySendMessage(objectName, methodName, [_error.description UTF8String]);
    }
    else
    {
        UnitySendMessage(objectName, methodName, "success");
    }
}
 
@end

これを実際に使うと以下のようになります。

CaptureCallback *callback = [[CaptureCallback alloc] initWithObjectName:@"obj" methodName:@"method"];

UIImageWriteToSavedPhotosAlbum(image, callback, @selector(savingImageIsFinished:didFinishSavingWithError:contextInfo:), nil);

詳細は以下のドキュメントをご覧ください。

developer.apple.com

保存パスをコールバックで受け取る

上記の関数では保存と保存後のコールバックを受け取れても、保存したファイルのパスを知ることができません。
これを実現するためにはPHPhotoLibraryperformChangesと、PHImageManagerrequestImageDataForAsset:を利用します。

コードは以下のようになります。
ポイントはいくつかの非同期関数を連続で呼び出し、最終的にファイルパスを取得している点です。

__block NSString* localId;

// Add it to the photo library
[PHPhotoLibrary.sharedPhotoLibrary performChanges:^{
    PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    
    localId = assetChangeRequest.placeholderForCreatedAsset.localIdentifier;
} completionHandler:^(BOOL success, NSError *err) {
    
    if (!success)
    {
        NSLog(@"Error saving image: %@", err.localizedDescription);
        [callback savingImageIsFinished:nil
                didFinishSavingWithError:err];
    }
    else
    {
        PHFetchResult* assetResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[localId] options:nil];
        PHAsset *asset = assetResult.firstObject;
        [PHImageManager.defaultManager requestImageDataForAsset:asset
                                                        options:nil
                                                    resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
                                                        
                                                        NSURL *fileUrl = [info objectForKey:@"PHImageFileURLKey"];
                                                        
                                                        if (fileUrl)
                                                        {
                                                            NSLog(@"Image path: %@", fileUrl.relativePath);
                                                            [callback savingImageIsFinished:fileUrl
                                                                    didFinishSavingWithError:nil];
                                                        }
                                                        else
                                                        {
                                                            NSLog(@"Error retrieving image filePath, heres whats available: %@", info);
                                                            [callback savingImageIsFinished:nil
                                                                    didFinishSavingWithError:nil];
                                                        }
                                                    }];
    }
}];

やや複雑ですが、これで保存先のパスを得ることができます。

まとめ

ひとまずここまでで、Unity側で生成したテクスチャをネイティブ側に送りファイルに保存することができました。
今回は保存することを目的に作成しましたが、ネイティブ側で加工をしたり、ネイティブ側で生成したものをUnity側に送るなど、活用の幅は広いと思います。

ネイティブと友だちになれるとやれることが格段に増えるのでぜひともマスターしておきたいですね。

そして以下からは、今回のプラグインを作るにあたってハマった点や試したことなど、後々役に立ちそうなものをメモとして残しておきます。
興味がある方は読んでみてください。


その他の役立ちそうなメモ

Native Plugin Interfaceについて

最後に、今回の実装とは直接は関係ありませんが、デバイス情報(MTLDevice)などを利用したいケースがある場合にUnityのAPIからそれらデバイスへの参照の取得方法を紹介していきます。

これらはNative Plugin InterfaceとしてUnityから提供されているAPIになります。

バイスへの参照を取得する

バイスへの参照などを得るためにUnityはIUnityInterfacesというインターフェースを用意しています。
これはプラグインが読み込まれた際に呼ばれるコールバック内で取得することができます。

ドキュメントにはextern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoadを公開しておけば自動的に呼ばれる、と記載されているのですがiOSだとダメなのかこれだけでは呼び出されませんでした。

そこで別の方法を利用してこのインターフェースを取得するようにしました。

具体的には以下の関数をあらかじめ呼び出すことで対応しました。

UnityRegisterRenderingPluginV5(&UnityPluginLoad, &UnityPluginUnload);

名前から分かる通り、プラグインが読み込まれた際に、引数に渡したコールバックが呼ばれる仕組みになっています。
この登録処理を、C#側から呼べるようにしておき、Startなどのタイミングで呼び出しておきます。

これら諸々を記述したコードは以下のようになります。

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces);
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload();

static IUnityGraphicsMetal* s_MetalGraphics = 0;
static IUnityInterfaces*    s_UnityInterfaces  = 0;
static IUnityGraphics*      s_Graphics = 0;

static bool initialized = false;

static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType)
{
    switch (eventType)
    {
        case kUnityGfxDeviceEventInitialize:
        {
            // s_RendererType = s_Graphics->GetRenderer();
            initialized = false;
            break;
        }
        case kUnityGfxDeviceEventShutdown:
        {
            // s_RendererType = kUnityGfxRendererNull;
            initialized = false;
            break;
        }
        case kUnityGfxDeviceEventBeforeReset:
        {
            // TODO: User Direct3D 9 code
            break;
        }
        case kUnityGfxDeviceEventAfterReset:
        {
            // TODO: User Direct3D 9 code
            break;
        }
    };
}

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
    s_UnityInterfaces = unityInterfaces;
    s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
    s_MetalGraphics   = s_UnityInterfaces->Get<IUnityGraphicsMetal>();

    s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
    OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload()
{
    s_Graphics->UnregisterDeviceEventCallback(OnGraphicsDeviceEvent);
}

///
/// Attach the functions to the callback of plugin loaded event.
///
extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API _AttachPlugin()
{
    UnityRegisterRenderingPluginV5(&UnityPluginLoad, &UnityPluginUnload);
}

特に大事な点は以下です。

extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
{
    s_UnityInterfaces = unityInterfaces;
    s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
    s_MetalGraphics   = s_UnityInterfaces->Get<IUnityGraphicsMetal>();

    s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
    OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
}

コールバックにはIUnityInterfacesが引数として渡されてくるので、さらにそこからunityInterfaces->Get<IUnityGraphicsMetal>();を呼び出すことでデバイスへの参照を持つオブジェクトを取得することができます。

UNITY_INTERFACE_EXPORTとUNITY_INTERFACE_API

ちなみにAPIを利用するにはUNITY_INTERFACE_EXPORTUNITY_INTERFACE_APIのマクロを付ける必要があります。
ぱっと見はなにをしてくれるものか分かりづらいので、定義元をメモしておきます。

定義を見てみると以下のように分岐されています。

#if defined(__CYGWIN32__)
    #define UNITY_INTERFACE_API __stdcall
    #define UNITY_INTERFACE_EXPORT __declspec(dllexport)
#elif defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(_WIN64) || defined(WINAPI_FAMILY)
    #define UNITY_INTERFACE_API __stdcall
    #define UNITY_INTERFACE_EXPORT __declspec(dllexport)
#elif defined(__MACH__) || defined(__ANDROID__) || defined(__linux__)
    #define UNITY_INTERFACE_API
    #define UNITY_INTERFACE_EXPORT
#else
    #define UNITY_INTERFACE_API
    #define UNITY_INTERFACE_EXPORT
#endif

プラットフォームによっては関数をDLLにexportするために必要な指定が必要なため、また指定方法が異なるためこういうスイッチが入っています。

iOSなどその他のプラットフォームでは特になにも出力されないので、読むときは単純に関数の宣言と見ておいて大丈夫です。

ちなみに__stdcallは関数の呼び出し規約となっていて、アセンブラレベルでは関数に対して引数をどう渡すか、また戻り値をどう受け取るか、という取り決めを事前にしておく必要があります。

その取り決めを明示するものであり、これがないと引数がうまく渡せなかったり、など問題がおきます。(多分、誤った指定だとエラーになるか、実行時にクラッシュします。試したこと無いので推測ですが・・・)

このあたりについては以下の記事が参考になるかもしれません。

qiita.com

ハマった点

Photo Libraryへのアクセス権

Photo Libraryに保存するためにアクセス権を取得する必要があります。
実行時にアクセス権を確認し、アクセス権がなければ適切にリクエストする必要があります。

ちなみに確認せずに実行するとクラッシュして以下のようなエラーが出力されます。

$ Photos Access not allowed

このあたりについては以下の記事を参考に修正しました。

superhahnah.com

その他メモ

Build Settingsをポストプロセスで更新する

Swiftコードも含んでいるためBuild Settingsを修正する必要があり、それを自動化するために以下の記事を参考にさせていただきました。

uwanosora22.hatenablog.com

MTLTextureをコピーする

実装する過程でクラッシュが発生したため、いったんテクスチャをネイティブ側でコピーして保持したらどうだ、っていうことで作ったものがあるのでメモとして残しておきます。

id<MTLTexture> CopyTexture(id<MTLTexture> source)
{
    MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
    descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
    descriptor.width = source.width;
    descriptor.height = source.height;
    
    id<MTLTexture> texture = [s_MetalGraphics->MetalDevice() newTextureWithDescriptor:descriptor];

    id<MTLCommandQueue> queue = [s_MetalGraphics->MetalDevice() newCommandQueue];
    id<MTLCommandBuffer> buffer = [queue commandBuffer];
    id<MTLBlitCommandEncoder> encoder = [buffer blitCommandEncoder];
    [encoder copyFromTexture:source
                 sourceSlice:0
                 sourceLevel:0
                sourceOrigin:MTLOriginMake(0, 0, 0)
                  sourceSize:MTLSizeMake(source.width, source.height, source.depth)
                   toTexture:texture
            destinationSlice:0
            destinationLevel:0
           destinationOrigin:MTLOriginMake(0, 0, 0)];
    [encoder endEncoding];
    [buffer commit];
    [buffer waitUntilCompleted];

    return texture;
}

MTLTextureをネイティブ側で生成してポインタを受け取る

今回の実装をしていく過程で、試しにネイティブ側でMTLTextureを生成してUnity側に渡したらうまくいくかも、と思って実装したのでメモとして残しておきます。

extern "C" uintptr_t UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API _GetNativeTexturePtr(int width, int height)
{
    MTLTextureDescriptor *descriptor = [[MTLTextureDescriptor alloc] init];
    descriptor.pixelFormat = MTLPixelFormatBGRA8Unorm;
    descriptor.width = width;
    descriptor.height = height;
    
    id<MTLTexture> texture = [s_MetalGraphics->MetalDevice() newTextureWithDescriptor:descriptor];

    return (uintptr_t)texture;
}

その他参考にした記事

tips.hecomi.com