この記事はNTTテクノクロス Advent Calendar 2023 シリーズ2の16日目の記事です。
こんにちは、NTTテクノクロスの西川と申します。
本記事ではたまたま自分が触る機会のあったUnityとネイティブプラグイン(C++とかで書かれたソースコードなど)を利用してMac/iOSでレンダリング(描画)するための方法を紹介していきたいと思います。
もしOpenCVとかのC++で書かれたソースコードから直接Unityのテクスチャをいじりたいとかマニアックなことをしたい場合に参考にしていただけると助かります。
また、本記事で紹介するソースコードはUnity-Technologiesが公開しているソースコードを参考にしています。
Unityのレンダリング基礎知識
そもそもUnityで3Dオブジェクトを表現する際には、メッシュ(Mesh)、マテリアル(Material)、シェーダー(Shader)、テクスチャ(Texture)という4つの要素を参照し、レンダリング処理をしています。
すごくざっくり解説すると、メッシュは3Dオブジェクトの形状について、マテリアルとシェーダーが実際の画面に描画する際にどういう計算をすればいいか(光の反射とかをどう計算すればいいかなど)について、テクスチャが3Dオブジェクトに貼り付ける画像情報についてを管理しています。(より詳細な情報を知りたい人はUnity公式のWebページ:https://docs.unity3d.com/ja/2019.4/Manual/Shaders.html
とかが参考になるかもしれません...)
ネイティブプラグインからレンダリングを操作するための準備(Mac/iOS)
ではここで、ネイティブプラグインから3Dオブジェクトの形状やテクスチャを変更したい場合を考えてみます。
ネイティブプラグインを使って実際にレンダリング処理に操作するにあたって先に知っておかなければいけない情報として、Unityが内部でどんなライブラリを利用してレンダリング処理を行っているのかを把握しておく必要があります。
Unityでレンダリング処理を実行するライブラリ(GraphicsAPI)はOSによって異なっており、著者環境のUnity 2022.3.14だと、
- Windows:Direct3D11, Direct3D12, Vulkan, OpenGLCore
- Android:OpenGLES3, OpenGLES2(Deprecated), Vulkan
- Mac:Metal, OpenGLCore(Deprecated)
- iOS:Metal
という実装状況になっており、この中のどれかで実際のレンダリング処理が行われています。
ここで、Mac/iOSで同じソースコードからレンダリング処理に操作を加えようとすると、必然的にMetalを利用したレンダリング処理に手を加える必要があります。
そこで今回はMacOS/iOSで共通のUnityプロジェクトとライブラリからレンダリング処理に手を加えてみたいと思います。
目標
今回はC#スクリプト側で用意した2枚の画像(☆と⚪︎を真ん中に配置した画像)と、2種類のメッシュの形状(◻︎と◇)を独自に設定し、それらを交互に表示させることを目標にしてみます。
Unity側の準備
まずはUnityのプロジェクトを作成します。(とりあえずスタンダードに3D + URPのテンプレートを利用)
次にUnityのシーンを作成します。
とりあえず必要なものとして、
- 実際にレンダリングするゲームオブジェクト
- レンダリング処理に手を加えるためのC#スクリプト
の2つだけのシンプルなシーンを構成してみようと思います。
まずは、余計な要素が何もないゲームオブジェクト(HierarchyからCreate Emptyで作成できるゲームオブジェクト)をシーンに追加します。
次にそのオブジェクトがメッシュを持つためにMeshFilter(UnityはMeshFilterがメッシュを管理している)と、そのメッシュを表示するために必要となるMeshRendererを作成したゲームオブジェクトにアタッチします。
次はレンダリングに利用するマテリアルを作成します。UnityエディタのProjectから適当な名前のマテリアルを作成して、先ほどゲームオブジェクトにアタッチしたMeshRendererのMaterialsに設定します。
その次は実際に操作するためのC#スクリプトをUnityエディタのProjectから生成し、以下のように書き換えます。
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Rendering;
public class MainScript : MonoBehaviour
{
public TextAsset[] textAssets; // 非圧縮の画像データ
private MeshFilter meshFilter;
private Mesh mesh;
private Texture2D tex;
private Material material;
List<float[]> myVertices = new List<float[]>() { // x, y, z, u, v
new float[]{ // ◇
0, 1, 0, 0, 0,
1, 0, 0, 1, 0,
-1, 0, 0, 0, 1,
1, 0, 0, 1, 0,
0, -1, 0, 1, 1,
-1, 0, 0, 0, 1
},
new float[]{ // ◻︎
-1, 1, 0, 0, 0,
1, 1, 0, 1, 0,
1, -1, 0, 1, 1,
-1, 1, 0, 0, 0,
1, -1, 0, 1, 1,
-1, -1, 0, 0, 1
}
};
int[] myTriangles = new int[]{0, 1, 2, 3, 4, 5};
IntPtr myRenderingFuncPtr = IntPtr.Zero;
// Start is called before the first frame update
void Start()
{
int width = (int)Mathf.Sqrt(textAssets[0].bytes.Length / 4.0f); // alphaチャネルまで想定
int height = width;
Debug.Log($"TextureSize: {width} x {height}");
Debug.Log(myVertices[0].Length);
// メッシュの設定
meshFilter = GetComponent<MeshFilter>();
mesh = new Mesh();
mesh.SetVertexBufferParams(0, // ここで頂点数を設定するだけだとNativePluginから操作できなかったため、0に設定し、後で頂点数を操作する
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3, stream: 0), // x,y,z
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2, stream: 0) // u,v
);
mesh.indexFormat = IndexFormat.UInt32; // 頂点インデックスを32bit精度に変更
mesh.MarkDynamic(); // メッシュを更新するために最適化
mesh.vertices = new Vector3[myVertices[0].Length / 5];
mesh.triangles = new int[myTriangles.Length];
mesh.uv = new Vector2[myVertices[0].Length / 5];
meshFilter.mesh = mesh;
// テクスチャの設定
tex = new Texture2D(width, height, TextureFormat.RGBA32, false); // メモリとしては"TextureFormat.RGB24"を指定してもalphaチャネルまで確保されるため"TextureFormat.RGBA32"を指定
material = this.GetComponent<MeshRenderer>().material;
material.SetTexture("_BaseMap", tex); // URPだと新規作成したMatirialが参照しているtextureの名称が"_BaseMap"のため"_BaseMap"を設定
// NativePluginへの設定
SetTexParam(width, height);
SetImage(textAssets[0].bytes, 0);
SetImage(textAssets[1].bytes, 1);
SetMeshParam(myVertices[0].Length / 5, myTriangles.Length);
SetMesh(myVertices[0], myTriangles, 0);
SetMesh(myVertices[1], myTriangles, 1);
SetTexHandle(tex.GetNativeTexturePtr());
SetMeshHandle(mesh.GetNativeVertexBufferPtr(0), mesh.GetNativeIndexBufferPtr());
myRenderingFuncPtr = GetRenderingFuncPtr();
StartCoroutine(MyCoroutine());
}
// Update is called once per frame
void Update()
{
}
IEnumerator MyCoroutine(){
int idx = 0;
while(true)
{
GL.IssuePluginEvent(myRenderingFuncPtr, idx);
idx = (idx + 1) % myVertices.Count;
yield return new WaitForSeconds(1);
}
}
#if (UNITY_IOS && !UNITY_EDITOR)
const string DllName = "__Internal"; // iOS
#else
const string DllName = "MyPlugin"; // Mac
#endif
[DllImport(DllName)]
public static extern void UnityPluginLoad(IntPtr unityInterfaces); // Unityアプリ起動時に呼び出される関数
[DllImport(DllName)]
public static extern void SetTexParam(int width, int height); // テクスチャパラメータの設定
[DllImport(DllName)]
public static extern void SetMeshParam(int vertexNum, int indexNum); // メッシュパラメータの設定
[DllImport(DllName)]
public static extern void SetImage(byte[] img, int imgIdx); // テクスチャに利用する画像データの設定用関数
[DllImport(DllName)]
public static extern void SetMesh(float[] vertexPtr, int[] indexPtr, int meshIdx); // メッシュに利用する頂点/インデックスの設定用関数
[DllImport(DllName)]
public static extern void SetTexHandle(IntPtr texhandle); // テクスチャ制御ハンドルの設定
[DllImport(DllName)]
public static extern void SetMeshHandle(IntPtr vertexhandle, IntPtr indexhandle); // メッシュ制御ハンドルの設定
[DllImport(DllName)]
public static extern IntPtr GetRenderingFuncPtr(); // 描画処理関数ポインタの取得
}
大まかな流れとしては、
- Unity側でレンダリングに使用するメッシュ/テクスチャを用意
- メッシュ/テクスチャの操作に必要なパラメータを設定
- 1秒間隔でメッシュ/テクスチャを書き換える命令を発行
という流れになっています。
以下では、もう少し詳細にC#スクリプトの解説をしますので、とりあえず動かしたい人は次のセクションまで読み飛ばしてください。
public TextAsset[] textAssets; // 非圧縮の画像データ保存用
TextAssetは生のbyte配列を扱うために利用するクラスで、UnityのC#スクリプトからネイティブプラグインに非圧縮の画像データ(pngとかjpgとかで圧縮されてないデータ)を利用するために使ってます。
そのため、ネイティブプラグイン内で画像生成から加工までを実施する場合はいらないですが、サンプルプログラムとしては、Unityから非圧縮画像データを渡します。
またこの際、諸事情で画像データはRGBだけでなくRGBAのアルファチャネルまで用意しています。(理由はネイティブプラグイン側の処理で説明します。)
List<float[]> myVertices = new List<float[]>() { // x, y, z, u, v
new float[]{ // ◇
0, 1, 0, 0, 0,
1, 0, 0, 1, 0,
-1, 0, 0, 0, 1,
1, 0, 0, 1, 0,
0, -1, 0, 1, 1,
-1, 0, 0, 0, 1
},
new float[]{ // ◻︎
-1, 1, 0, 0, 0,
1, 1, 0, 1, 0,
1, -1, 0, 1, 1,
-1, 1, 0, 0, 0,
1, -1, 0, 1, 1,
-1, -1, 0, 0, 1
}
};
今回は2種類の四角形のメッシュを生成するために6頂点分のデータを2つC#スクリプト内部で確保しています。
内容としては頂点の位置座標であるx, y, z座標と、頂点とテクスチャとの対応マップであるu, v座標を1組として5要素 x 6頂点 = 30 個分のfloat変数を確保している配列を用意しています。もし、光の反射を計算するために法線ベクトルが必要だったり、頂点そのものに色情報を持たせたりしたい場合は適宜要素を追加してください。
(一つ目のfloat配列が◇の形状のメッシュを、二つ目のfloat配列が◻︎の形状のメッシュを表現している。)
int[] myTriangles = new int[]{0, 1, 2, 3, 4, 5};
頂点同士がどのように繋がって面を表現するのかに必要なインデックス情報を用意しています。
3Dオブジェクトが面を表現するために、3つの頂点で1つの三角形の面を表現しています。
(今回だと、頂点0, 1, 2で一つの三角形、3, 4, 5で一つの三角形で2つ分の三角形を表現しています。)
より詳しい説明はUnity公式で解説されてます。(https://docs.unity3d.com/ja/2022.1/Manual/AnatomyofaMesh.html )
IntPtr myRenderingFuncPtr = IntPtr.Zero;
ネイティブプラグインの関数からレンダリング処理を実施する場合は、Update関数内から直接呼び出すのではなく、コールバック関数として登録しておいた関数をUnityが適切なタイミングで呼び出すという手順を踏む必要があります。
そのため、ネイティブプラグインから取得できる関数をコールバック関数として登録する際に利用する関数ポインタ変数をここで用意しておきます。
int width = (int)Mathf.Sqrt(textAssets[0].bytes.Length / 4.0f); // alphaチャネルまで想定
int height = width;
今回利用する画像の高さと幅を計算しています。
今回利用する画像は幅と高さが同じですが、画像サイズを任意にしたい場合は適切な値を設定してください。
// メッシュの設定
meshFilter = GetComponent<MeshFilter>();
mesh = new Mesh();
mesh.SetVertexBufferParams(0, // ここで頂点数を設定するだけだとNativePluginから操作できなかったため、0に設定し、後で頂点数を操作する
new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3, stream: 0), // x,y,z
new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2, stream: 0) // u,v
);
ここで頂点データがどのようなプロパティ(情報)を持っているのかを設定しています。
今回はxyz座標とuv座標を想定しているため、そのように設定しました。
mesh.indexFormat = IndexFormat.UInt32; // 頂点インデックスを32bit精度に変更
mesh.MarkDynamic(); // メッシュを更新するために最適化
元々頂点のインデックス情報が16bit精度で確保されているので、データの書き換えがmemcpyだけで済むようにインデックス情報を32bit精度に変更しています。
また、メッシュを頻繁に更新する場合はMesh.MarkDynamicメソッドを呼び出しておくと効率が上昇すると、Unity公式が言っている(https://docs.unity3d.com/ja/2021.1/ScriptReference/Mesh.MarkDynamic.html) ため、ここで呼び出しておきます。
mesh.vertices = new Vector3[myVertices[0].Length / 5];
mesh.triangles = new int[myTriangles.Length];
mesh.uv = new Vector2[myVertices[0].Length / 5];
ここで、操作する頂点の数を決めて、Unityが確保するメッシュの最終的な書き込み先のデータ領域を確保しています。
tex = new Texture2D(width, height, TextureFormat.RGBA32, false); // メモリとしては"TextureFormat.RGB24"を指定してもalphaチャネルまで確保されるため"TextureFormat.RGBA32"を指定
ここでは、Unityが確保する最終的なテクスチャの書き込み先のデータ領域を確保しています。
また、今回はテクスチャのフォーマットをTextureFormat.RGBA32としていますが、RGB24としても、Unityが内部で確保するテクスチャのデータ領域の構造は変化しないため(実際にRGB24で試しても変わらなかった)、どちらを設定しても問題なく動作します。
今回はわかりやすいようにネイティブプラグイン側の処理に合わせてTextureFormat.RGBA32としました。
material.SetTexture("_BaseMap", tex); // URPだと新規作成したMatirialが参照しているtextureの名称が"_BaseMap"のため"_BaseMap"を設定
マテリアルに適用されるテクスチャと、C#スクリプトで作成したテクスチャを紐づけています。
この際に設定している_BaseMapという名称は使用するシェーダーによって異なりますが、シェーダーに「Universal Render Pipeline/Lit」というシェーダーを設定していれば_BaseMapという名称で大丈夫です。
もし別のシェーダーを利用する場合は、適宜シェーダーの中身をテキストエディタなどから確認してみると名称がわかります。
SetTexParam(width, height);
SetImage(textAssets[0].bytes, 0);
SetImage(textAssets[1].bytes, 1);
SetMeshParam(myVertices[0].Length / 5, myTriangles.Length);
SetMesh(myVertices[0], myTriangles, 0);
SetMesh(myVertices[1], myTriangles, 1);
メッシュ/テクスチャをネイティブプラグインで操作するために、書き換えるためのデータを設定したり、書き換えに必要な画像のサイズなどの情報を設定したりしています。(実際に関数でどういう処理をしているかはネイティブプラグイン側のソースコードをみてください。)
SetTexHandle(tex.GetNativeTexturePtr());
SetMeshHandle(mesh.GetNativeVertexBufferPtr(0), mesh.GetNativeIndexBufferPtr());
Unityが確保しているメッシュ/テクスチャにアクセスするために必要な情報をネイティブプラグインへ渡しています。
実際にどういうデータがやりとりされているかはGraphicsAPI次第ですが、Metalは生のポインタ値を渡しているみたいでした。
myRenderingFuncPtr = GetRenderingFuncPtr();
ネイティブプラグインが提供するレンダリング処理を実施する関数のポインタ値を渡しています。
GL.IssuePluginEvent(myRenderingFuncPtr, idx);
コールバック関数をレンダリング処理に反映できるタイミングで実行するように予約しています。
そのため、すぐにコールバック関数が実行されるわけではなく、レンダリング処理に反映できる適切なタイミングでUnityがコールバック関数を実行してくれます。
#if (UNITY_IOS && !UNITY_EDITOR)
const string DllName = "__Internal"; // iOS
#else
const string DllName = "MyPlugin"; // Mac
#endif
[DllImport(DllName)]
public static extern void UnityPluginLoad(IntPtr unityInterfaces); // Unityアプリ起動時に呼び出される関数
[DllImport(DllName)]
public static extern void SetTexParam(int width, int height); // テクスチャパラメータの設定
[DllImport(DllName)]
public static extern void SetMeshParam(int vertexNum, int indexNum); // メッシュパラメータの設定
[DllImport(DllName)]
public static extern void SetImage(byte[] img, int imgIdx); // テクスチャに利用する画像データの設定用関数
[DllImport(DllName)]
public static extern void SetMesh(float[] vertexPtr, int[] indexPtr, int meshIdx); // メッシュに利用する頂点/インデックスの設定用関数
[DllImport(DllName)]
public static extern void SetTexHandle(IntPtr texhandle); // テクスチャ制御ハンドルの設定
[DllImport(DllName)]
public static extern void SetMeshHandle(IntPtr vertexhandle, IntPtr indexhandle); // メッシュ制御ハンドルの設定
[DllImport(DllName)]
public static extern IntPtr GetRenderingFuncPtr(); // 描画処理関数ポインタの取得
C#スクリプトにネイティブプラグインで定義した関数のシンボル情報を設定しています。
またMacとiOSでネイティブプラグインの関数呼び出しを動的/静的に組み込んだりする差異があるため、マクロで呼び出し方を制御しています。
ネイティブプラグイン側の準備
ネイティブプラグインのソースコードを書くにあたって、xcodeのプロジェクトを以下のような構成で用意します。
この際、Unityフォルダーにいくつかのヘッダーファイルがありますが、これはUnity公式がネイティブプラグインから直接メッシュ/テクスチャを操作するためにgithubに公開しているサンプルコード( https://github.com/Unity-Technologies/NativeRenderingPlugin.git )から必要なファイルをダウンロードして配置します。
次にメッシュ/テクスチャを書き換える処理を実施するネイティブプラグインのヘッダーとソースコードを書きます。
#include "Unity/IUnityGraphics.h"
extern "C"{
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces); // アプリ開始時に自動で呼ばれ、GraphicsAPIの操作に必要なIUnityInterfacesクラスへのアクセスを可能とする
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTexParam(int width, int height); // 操作対象となるテクスチャのパラメータ設定
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMeshParam(int vertexNum, int indexNum); // 操作対象となるメッシュのパラメータ設定
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetImage(unsigned char* img, int imgIdx); // 実際にレンダリングする非圧縮画像データの設定
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMesh(float* vertexPtr, int* indexPtr, int meshIdx); // 実際にレンダリングするメッシュデータの設定
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTexHandle(void* texhandle); // テクスチャ操作に利用するハンドルの設定
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMeshHandle(void* vertexhandle, void* indexhandle); // メッシュ操作に利用するハンドルの設定
UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderingFuncPtr(); // コールバック関数の関数ポインタ取得
static void UpdateMeshTex(int eventID); // コールバック関数
static void UpdateTex(int eventID); // テクスチャ更新関数
static void UpdateMesh(int eventID); // メッシュ更新関数
}
#include "MyPlugin.h"
#include <vector>
#include "Unity/IUnityGraphicsMetal.h"
#import <Metal/Metal.h>
#include "Unity/PlatformBase.h"
#include <stdio.h>
#define TEX_NUM 2
// GraphicsAPIを操作するためにUnityが用意しているクラス(今回は未使用)
static IUnityInterfaces* g_unityInterfaces;
// 生データ書き込み領域
static std::vector<std::vector<unsigned char>> g_rawImages(TEX_NUM); // 非圧縮画像
static std::vector<std::vector<float>> g_vertex(TEX_NUM); // 頂点データ
static std::vector<std::vector<int>> g_index(TEX_NUM); // インデックス情報
// Unityが確保しているメッシュ/テクスチャハンドル
static void* g_texHandle = nullptr; // テクスチャへのアクセスハンドル
static void* g_vertexHandle = nullptr; // 頂点データへのアクセスハンドル
static void* g_indexHandle = nullptr; // インデックスデータへのアクセスハンドル
// テクスチャのパラメータ
static int g_texWidth = 0; // 画像高さ
static int g_texHeight = 0; // 画像幅
static int g_texLength = 0; // チャンネル数まで考慮した非圧縮画像のメモリサイズ
// メッシュのパラメータ
static int g_vertexNum = 0; // 頂点数
static int g_vertexMemSize = 0; // 頂点の持つ情報まで考慮したメモリサイズ
static int g_indexNum = 0; // インデックス数
static int g_indexMemSize = 0; // インデックスが確保されている型まで考慮したメモリサイズ
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces) {
g_unityInterfaces = unityInterfaces;
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTexParam(int width, int height) {
g_texWidth = width;
g_texHeight = height;
g_texLength = width * height * 4; // alphaチャネルまで考慮して非圧縮画像を保存するデータサイズを決める
for(int i = 0; i < g_rawImages.size(); i++){
g_rawImages[i].clear();
g_rawImages[i].resize(g_texLength);
}
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMeshParam(int vertexNum, int indexNum) {
g_vertexNum = vertexNum;
g_indexNum = indexNum;
g_vertexMemSize = vertexNum * sizeof(float) * 5; // x, y, z, u, v座標で1頂点のため x5 を実施
g_indexMemSize = indexNum * sizeof(int); // 今回はC#スクリプトで1インデックスあたり32bit精度で確保しているためそれを考慮する
for(int i = 0; i < g_vertex.size(); i++){
g_vertex[i].clear();
g_vertex[i].resize(g_vertexMemSize);
g_index[i].clear();
g_index[i].resize(g_indexMemSize);
}
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetImage(unsigned char* img, int imgIdx) {
memcpy(g_rawImages[imgIdx].data(), img, g_texLength);
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMesh(float* vertexPtr, int* indexPtr, int meshIdx) {
memcpy(g_vertex[meshIdx].data(), vertexPtr, g_vertexMemSize);
memcpy(g_index[meshIdx].data(), indexPtr, g_indexMemSize);
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTexHandle(void* texhandle) {
g_texHandle = texhandle;
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMeshHandle(void* vertexhandle, void* indexhandle){
g_vertexHandle = vertexhandle;
g_indexHandle = indexhandle;
}
UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderingFuncPtr() {
return &UpdateMeshTex;
}
static void UpdateMeshTex(int eventID){
UpdateMesh(eventID); // メッシュの更新
UpdateTex(eventID); // テクスチャの更新
}
static void UpdateTex(int eventID) {
const int rowPitch = g_texWidth * 4; // RGBAチャネルで確保されていることを想定
id<MTLTexture> tex = (__bridge id<MTLTexture>)g_texHandle; // C#スクリプトから受け取ったテクスチャへのハンドルをキャストしてMTLTextureへのアクセスを可能とする
[tex replaceRegion:MTLRegionMake2D(0,0, g_texWidth, g_texHeight) mipmapLevel:0 withBytes:g_rawImages[eventID].data() bytesPerRow:rowPitch];
}
static void UpdateMesh(int eventID){
id<MTLBuffer> buf = nullptr;
void* dstPtr = nullptr;
size_t len = 0;
// vertexの更新
buf = (__bridge id<MTLBuffer>)g_vertexHandle; // C#スクリプトから受け取った頂点データへのハンドルをキャストしてMTLBufferへのアクセスを可能とする
dstPtr = [buf contents];
len = buf.length;
memcpy(dstPtr, g_vertex[eventID].data(), len);
#if UNITY_OSX
[buf didModifyRange:NSMakeRange(0, len)]; // Macのみバッファの書き換えを通知する
#endif
// indexの更新
buf = (__bridge id<MTLBuffer>)g_indexHandle; // C#スクリプトから受け取ったインデックスデータへのハンドルをキャストしてMTLBufferへのアクセスを可能とする
dstPtr = [buf contents];
len = buf.length;
memcpy(dstPtr, g_index[eventID].data(), len);
#if UNITY_OSX
[buf didModifyRange:NSMakeRange(0, len)]; // Macのみバッファの書き換えを通知する
#endif
}
以下で詳細にプログラムの解説をします。
// GraphicsAPIを操作するためにUnityが用意しているクラス(今回は未使用)
static IUnityInterfaces* g_unityInterfaces;
// 生データ書き込み領域
static std::vector<std::vector<unsigned char>> g_rawImages(TEX_NUM); // 非圧縮画像
static std::vector<std::vector<float>> g_vertex(TEX_NUM); // 頂点データ
static std::vector<std::vector<int>> g_index(TEX_NUM); // インデックス情報
// Unityが確保しているメッシュ/テクスチャハンドル
static void* g_texHandle = nullptr; // テクスチャへのアクセスハンドル
static void* g_vertexHandle = nullptr; // 頂点データへのアクセスハンドル
static void* g_indexHandle = nullptr; // インデックスデータへのアクセスハンドル
// テクスチャのパラメータ
static int g_texWidth = 0; // 画像高さ
static int g_texHeight = 0; // 画像幅
static int g_texLength = 0; // チャンネル数まで考慮した非圧縮画像のメモリサイズ
// メッシュのパラメータ
static int g_vertexNum = 0; // 頂点数
static int g_vertexMemSize = 0; // 頂点の持つ情報まで考慮したメモリサイズ
static int g_indexNum = 0; // インデックス数
static int g_indexMemSize = 0; // インデックスが確保されている型まで考慮したメモリサイズ
ネイティブプラグイン内部で参照するリソース/プロパティをグローバル変数で宣言しています。
void UNITY_INTERFACE_API UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces) {
g_unityInterfaces = unityInterfaces;
}
今回は利用しなかったですが、自前で別のテクスチャやバッファを用意する際に利用したり、一部のGraphicsAPIでメッシュ/テクスチャを書き換えたりする際にUnityが用意しているIUnityInterfacesクラスへのポインタを受け取るための関数です。
この関数はUnityPluginLoadという名称で定義しておく必要があり、この名称の関数はUnityで生成したアプリが起動する際に自動で関数呼び出しを実施してくれます。
ただし、iOSだけは自動で呼ばれないため、IUnityInterfacesクラスを利用したい場合は注意が必要です。
(詳細は省きますが、Unityが用意しているUnityAppControllerというクラスを継承して、関数を登録したりする必要があります。)
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTexParam(int width, int height) {
g_texWidth = width;
g_texHeight = height;
g_texLength = width * height * 4; // alphaチャネルまで考慮して非圧縮画像を保存するデータサイズを決める
for(int i = 0; i < g_rawImages.size(); i++){
g_rawImages[i].clear();
g_rawImages[i].resize(g_texLength);
}
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMeshParam(int vertexNum, int indexNum) {
g_vertexNum = vertexNum;
g_indexNum = indexNum;
g_vertexMemSize = vertexNum * sizeof(float) * 5; // x, y, z, u, v座標で1頂点のため x5 を実施
g_indexMemSize = indexNum * sizeof(int); // 今回はC#スクリプトで1インデックスあたり32bit精度で確保しているためそれを考慮する
for(int i = 0; i < g_vertex.size(); i++){
g_vertex[i].clear();
g_vertex[i].resize(g_vertexMemSize);
g_index[i].clear();
g_index[i].resize(g_indexMemSize);
}
}
メッシュ/テクスチャのパラメータをネイティブプラグインに保存しています。
ここで保存した数値から、実際にレンダリング処理を実施するにあたって必要な引数に値を渡します。
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetImage(unsigned char* img, int imgIdx) {
memcpy(g_rawImages[imgIdx].data(), img, g_texLength);
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMesh(float* vertexPtr, int* indexPtr, int meshIdx) {
memcpy(g_vertex[meshIdx].data(), vertexPtr, g_vertexMemSize);
memcpy(g_index[meshIdx].data(), indexPtr, g_indexMemSize);
}
C#スクリプトから受け取った非圧縮画像とメッシュの構成に必要な頂点データとインデックスデータをネイティブプラグイン内に保存しています。
今回レンダリング処理をネイティブプラグインで実施するにあたって利用したGL.IssuePluginEvent関数が、コールバック関数のポインタとint型の数値しか指定できないため、事前にデータをネイティブプラグイン側に保存します。
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTexHandle(void* texhandle) {
g_texHandle = texhandle;
}
void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetMeshHandle(void* vertexhandle, void* indexhandle){
g_vertexHandle = vertexhandle;
g_indexHandle = indexhandle;
}
C#スクリプトで取得したメッシュ/テクスチャへのハンドルをネイティブプラグインに保存しています。
こちらもGL.IssuePluginEvent関数の制約で事前にネイティブプラグインに保存するという実装方法になっています。
UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderingFuncPtr() {
return &UpdateMeshTex;
}
ネイティブプラグインからレンダリング処理を実施するコールバック関数へのポインタを渡しています。
ここで戻り値として渡されているUpdateMeshTex関数へのポインタを基に、UpdateMeshTex関数がUnityのC#スクリプトで実際に呼び出されます。
static void UpdateMeshTex(int eventID){
UpdateMesh(eventID); // メッシュの更新
UpdateTex(eventID); // テクスチャの更新
}
GL.IssuePluginEvent関数が実際に呼び出すコールバック関数の本体です。
メッシュ/テクスチャ更新の処理実態は関数内で呼び出しているUpdateTex関数とUpdateMesh関数で実施しています。
また、この関数はGL.IssuePluginEventメソッドの引数の関係上、必ずint型の引数を1つだけ持たなければいけません。
static void UpdateTex(int eventID) {
const int rowPitch = g_texWidth * 4; // RGBAチャネルで確保されていることを想定
id<MTLTexture> tex = (__bridge id<MTLTexture>)g_texHandle;
[tex replaceRegion:MTLRegionMake2D(0,0, g_texWidth, g_texHeight) mipmapLevel:0 withBytes:g_rawImages[eventID].data() bytesPerRow:rowPitch];
}
この関数で実際にUnityが確保しているテクスチャ領域にデータを書き込んでいます。
この際、UnityのC#スクリプト側でテクスチャのフォーマットを"TextureFormat.RGB24"に設定しても内部的にはアルファチャネルまで確保されるという挙動のため、ネイティブプラグイン側ではアルファチャネルまで想定したデータサイズでデータのコピーを実施します。
実際にUnityが確保しているテクスチャ領域へのアクセスは
- C#スクリプトで設定テクスチャのポインタ値をMTLTextureへのポインタ値へとキャスト
- MTLTextureクラスのreplaceRegionメソッドでテクスチャを書き換え
という流れで実施しています。
また今回は、レンダリングに関する処理を実施するネイティブプラグインの関数はGL.IssuePluginEvent関数で呼び出すため、int型の数値を1つのみを引数にして、レンダリングするメッシュ/テクスチャを切り替えています。
static void UpdateMesh(int eventID){
id<MTLBuffer> buf = nullptr;
void* dstPtr = nullptr;
size_t len = 0;
// vertexの更新
buf = (__bridge id<MTLBuffer>)g_vertexHandle;
dstPtr = [buf contents];
len = buf.length;
memcpy(dstPtr, g_vertex[eventID].data(), len);
#if UNITY_OSX
[buf didModifyRange:NSMakeRange(0, len)];
#endif
// indexの更新
buf = (__bridge id<MTLBuffer>)g_indexHandle;
dstPtr = [buf contents];
len = buf.length;
memcpy(dstPtr, g_index[eventID].data(), len);
#if UNITY_OSX
[buf didModifyRange:NSMakeRange(0, len)];
#endif
}
ネイティブプラグインからのメッシュの頂点データとインデックスデータへの書き換えもテクスチャと概ね同じ流れで実施していますが、
- C#スクリプトから受け取ったポインタ値をMTLBufferへのポインタ値へとキャスト
- MTLBufferのcontentsメソッドから書き換え先のポインタ値を取得
- memcpyでメッシュを書き換え
- (Macのみ)MTLBufferのdidModifyRangeメソッドでメッシュの書き換えを通知する
という流れになっています。
ここでMacだけMTLBufferのdidModifyRangeというメソッドを呼んでいます。
これはMacだとUnityが確保しているMTLBufferがのMTLStorageModeがManagedというモードで確保されており、このモードのMTLBufferはdidModifyRangeというメソッドを呼び出すことで変更があったことを通知する必要があるらしいです。
(参考:https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/ResourceOptions.html )
実行結果
実行前にC#スクリプトをアタッチしたゲームオブジェクトに対して、実際に描画する非圧縮画像を設定します。
今回はtex1にオレンジ色の背景に青色の⚪︎の画像を、tex2に黄緑色の背景に黄色の☆の画像を設定しました。
実際に動作させると以下のようになります。
(オレンジが茶色に見えるのはシェーダーの計算でオレンジを表現する際に色が暗くなっているせいだと思います...)
まとめ
今回はMac/iOSでC++などのC#以外のソースコードからUnityのメッシュ/テクスチャを直接操作する方法を記事にしてみました。
Unity公式が公開しているサンプルコードには、他のGraphicsAPIの場合に参考にできるソースコードのサンプルもあります。今回の記事と合わせると、簡単にメッシュ/テクスチャの操作ができるようになると思いますので、よければ別のOSからMetalとは違ったGraphicsAPIを利用してメッシュ/テクスチャを書き換えてみてください。
参考
本記事で紹介したネイティブプラグインのソースコードはMITライセンスが付与されているUnity-Technologiesが公開しているソースコードを参考にしています。
参考にしたソースコード: https://github.com/Unity-Technologies/NativeRenderingPlugin © 2016, Unity Technologies
MITライセンス全文: https://opensource.org/license/mit/
おまけ
自分がRGBのpngファイルからRGBAの非圧縮画像データへの変換に利用したPythonのサンプルコードを載せておきます。
これを流用すれば、jpgやpng形式の画像を本記事で利用したRGBAの非圧縮画像データに変換できます。
import cv2
import io
import numpy as np
fileSymbols = ["tex1", "tex2"]
for fileSymbol in fileSymbols:
image = cv2.imread(fileSymbol + ".png")
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
alpha = np.full((image.shape[0], image.shape[1], 1), 255, dtype=np.uint8)
image = np.concatenate([image, alpha], 2)
print(image.shape)
print(image.dtype)
image = image.tobytes()
with open(fileSymbol + ".bytes", "wb") as f: # UnityのTextAssetクラスで扱うためにファイル拡張子を.bytesにしている
f.write(image)