LoginSignup
11
10

More than 3 years have passed since last update.

RustでUnityプラグインを作って敗北する

Posted at

なんか最近流行ってるらしい

浮世の変化には疎いのですが、なんか流行ってるらしいですねRust
実はUnityのネイティブプラグインを作ってみたかったのですが、CC++もやったことない上に勉強する気もないため踏み切れないでいました。
いい機会なのでRustで作ってみます。

目的

  • Rustを使ってみる
  • Unityのネイティブプラグインを作ってみる
  • 自分の学習軌道をメモしておく

書いている人

スマホ開発がメイン
CC++は未経験
値渡しと参照渡しはわかるけどぽいんた? とかいうのはわからん

情報取得

とりあえずインプットします。

Rust

Rustの日本語ドキュメント/Japanese Docs for Rust
プログラミング言語 Rust, 2nd Edition/ The Rust Programming Language, Second Edition
プログラミング言語Rust
The Rust Programming Language

必修言語Rustの他己紹介
Rust についてのメモ
Rustのポインタ(所有権・参照)・可変性についての簡単なまとめ
Rustはこうやって勉強するといいんじゃないか、という一例
Rustのクレート・ツールを探すための情報源

コンパイル通るまで殴ればいい! かんたん!
こんなにやさしいコンパイラ初めて見ました。cargoによる依存関係の管理もよく練れていていい感じです。
Kotlinなんかもそうですが近代の言語はユーザ獲得のため、チュートリアルがおそろしく充実していていいですね。
あとプロジェクト新規作成したらgit作ってくれるのすごい。

NativePlugin

Rustで実装したアルゴリズムをUnityから使う
C#からC++(DLL)に配列を渡す
How do I get Rust FFI to return array of structs or update memory?
How to return an array of structs from Rust to C#

プラグイン更新のたびにUnityEditorの再起動が必要とかつらい。

エディタ

intellij製品じゃないともうなにも書けない。
MEET INTELLIJ RUST

Vector3を100倍して返す

ひとまず手始めとして、C#から渡されたVector3Rust側で100倍して返します。

とりあえず作る

Rust側
// こんな感じで構造体を定義
#[repr(C)]
pub struct Vector3 {
    x: f32,
    y: f32,
    z: f32,
}

// 外部公開する関数
#[no_mangle]
pub extern fn size_up(v: &Vector3) -> Vector3 {
    Vector3 { x: v.x * 100.0, y: v.y * 100.0, z: v.z * 100.0 }
}
C#側
[DllImport("libtest")]
private static extern Vector3 size_up(Vector3 moto);

さっそく実行してみましょう。

テストコード
var v3 = UnityEngine.Random.insideUnitSphere;
var sizeUpV3 = size_up(v3);
Debug.Log($"{v3} -> {sizeUpV3}");
実行結果
(-0.1, -0.8, -0.2) -> (254563.7, 0.0, 1402342000000000000000000.0)

……なんか……なんだろう、よくないことが起こっているようですね。

借用をやめる

というわけで、Rust側を修正します。

引数を修正
#[no_mangle]
pub extern fn size_up(v: Vector3) -> Vector3 {
    Vector3 { x: v.x * 100.0, y: v.y * 100.0, z: v.z * 100.0 }
}

引数のv: Vector3をうっかり拝借していましたが、Rust内で完結するならばともかくC#から借りてくるのはあまりにも無茶な話でした。というわけでそのまま渡してみます。

実行結果
(-0.3, -0.7, 0.2) -> (-26.1, -66.8, 21.3)

……またよくないことが……いや、よく見たら四捨五入してそうな数字です。

nicely formatted

Rider先生にデコンパイルしてもらってVector3ToStringを覗いてみます。

Vector3.ToString
/// <summary>
///   <para>Returns a nicely formatted string for this vector.</para>
/// </summary>
/// <param name="format"></param>
public override string ToString()
{
  return UnityString.Format("({0:F1}, {1:F1}, {2:F1})", (object) this.x, (object) this.y, (object) this.z);
}

nicely formatted string

${\Large なに言うとるがじゃ!!!!!}$

しょうがないのでこんな感じの拡張メソッド定義してありのままの姿を見せてもらうことにします。

ToStringFloat
static class Extensions
{
    public static string ToStringFloat(this Vector3 vector3)
    {
        return $"({vector3.x}, {vector3.y}, {vector3.z})";
    }
}
テストコード
var v3 = UnityEngine.Random.insideUnitSphere;
var sizeUpV3 = size_up(v3);
Debug.Log($"{v3.ToStringFloat()} -> {sizeUpV3.ToStringFloat()}");
実行結果
(-0.01608862, 0.5905958, 0.7266953) -> (-1.608862, 59.05958, 72.66953)

できました。

気になるのは借用をやめた修正です。
C#側では「もともとのVector3」「引数としてコピーされたVector3」の2つがあります。
「もともとのVector3」はC#が管理しているからいいとして、Rustの借用ではないということは「引数としてコピーされたVector3」をRust側で開放しちゃってそうな気がしますが、これってC#側の扱いはどうなっているのでしょうか。externだとそのあたり忖度されるんでしょうか。もしくはstructなのでC#から渡すときに値をコピーしてるから大丈夫なのか。まあいいか。

計測

ようやくNative Pluginを使いたい理由に入ります。
Meshの頂点座標を基準点からの相対位置に変換する処理ですが、この処理がやや重い……ような気がします。そう頻繁に行う処理でもないので無理に高速化する必要もないのですが、今回はやってみることそのものが目的です。
というわけで、以下のC#で書かれた関数をRust側へ計算処理を逃がす関数にするのが今回のゴールです。

TransWithCsharp
public static Vector3[] TransWithCsharp(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = matrix.MultiplyPoint(points[count]);
    }
    return ret;

    // LINQでこう書くと実際オサレ
    // return points.Select(matrix.MultiplyPoint).ToArray();
}

matrix4x4

C#からMatrix4x4を受け取るため、Rust側で同じ構造体を定義します。
本来ならありものを使うのではなく、C#側でも自分でちゃんと受け渡すための構造体を定義するべきですが、Vector3もそのまま渡せたんだからMatrix4x4も行けるやろ! の精神です。

Matrix4x4
#[repr(C)]
pub struct Matrix4x4 {
    m00: f32,
    m01: f32,
    m02: f32,
    m03: f32,
    m10: f32,
    m11: f32,
    m12: f32,
    m13: f32,
    m20: f32,
    m21: f32,
    m22: f32,
    m23: f32,
    m30: f32,
    m31: f32,
    m32: f32,
    m33: f32,
}

ちゃんとRust側で受け取れているか試すために、以下の関数を作ってC#と突き合わせてみます。

#[no_mangle]
pub extern fn matrix_add(matrix: Matrix4x4) -> Vector3 {
    Vector3 { x: matrix.m00, y: matrix.m01, z: matrix.m02 }
}
テストコード
var Anchor = new GameObject().transform;
Anchor.position = UnityEngine.Random.insideUnitSphere;
Anchor.rotation = UnityEngine.Random.rotation;
Anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = Anchor.transform.localToWorldMatrix;

var a = matrix_add(matrix);
Debug.Log(a.ToStringFloat());
var b = new Vector3(matrix.m00, matrix.m01, matrix.m02);
Debug.Log(b.ToStringFloat());
実行結果
(-0.9590587, -0.6367525, -0.6988028)
(-0.9590587, -0.2962521, 0.6547196)

最初だけ合っている。ということはつまり構造体のメンバの定義されている順番が違うのでしょう。
再びRider先生にデコンパイルしてもらいます。

  public struct Matrix4x4 : IEquatable<Matrix4x4>
  {
    [NativeName("m_Data[0]")]
    public float m00;
    [NativeName("m_Data[1]")]
    public float m10;
    [NativeName("m_Data[2]")]
    public float m20;
    [NativeName("m_Data[3]")]
    public float m30;
    [NativeName("m_Data[4]")]
    public float m01;
    [NativeName("m_Data[5]")]
    public float m11;
    [NativeName("m_Data[6]")]
    public float m21;
    [NativeName("m_Data[7]")]
    public float m31;
    [NativeName("m_Data[8]")]
    public float m02;
    [NativeName("m_Data[9]")]
    public float m12;
    [NativeName("m_Data[10]")]
    public float m22;
    [NativeName("m_Data[11]")]
    public float m32;
    [NativeName("m_Data[12]")]
    public float m03;
    [NativeName("m_Data[13]")]
    public float m13;
    [NativeName("m_Data[14]")]
    public float m23;
    [NativeName("m_Data[15]")]
    public float m33;
  }

十の位から増えてるの……?
なんか感覚と違いますが、そう定義されている以上はしょうがありません。Rust側の構造体の定義の順番を変えます。

#[repr(C)]
pub struct Matrix4x4 {
    m00: f32,
    m10: f32,
    m20: f32,
    m30: f32,
    m01: f32,
    m11: f32,
    m21: f32,
    m31: f32,
    m02: f32,
    m12: f32,
    m22: f32,
    m32: f32,
    m03: f32,
    m13: f32,
    m23: f32,
    m33: f32,
}
実行結果
(-0.04724042, -0.6401328, 0.3618424)
(-0.04724042, -0.6401328, 0.3618424)

一致しました。Matrix4x4はちゃんとC#からRustに渡せています。

ダブルキャスト

/// <summary>
///   <para>Transforms a position by this matrix (generic).</para>
/// </summary>
/// <param name="point"></param>
public Vector3 MultiplyPoint(Vector3 point)
{
    Vector3 vector3;
    vector3.x = (float) ((double) this.m00 * (double) point.x + (double) this.m01 * (double) point.y + (double) this.m02 * (double) point.z) + this.m03;
    vector3.y = (float) ((double) this.m10 * (double) point.x + (double) this.m11 * (double) point.y + (double) this.m12 * (double) point.z) + this.m13;
    vector3.z = (float) ((double) this.m20 * (double) point.x + (double) this.m21 * (double) point.y + (double) this.m22 * (double) point.z) + this.m23;
    float num = 1f / ((float) ((double) this.m30 * (double) point.x + (double) this.m31 * (double) point.y + (double) this.m32 * (double) point.z) + this.m33);
    vector3.x *= num;
    vector3.y *= num;
    vector3.z *= num;
    return vector3;
}

肝心のMultiplyPointの処理です。
デコンパイラの結果というのもあると思いますが、なかなかにカオスな計算処理。
float -> double -> floatとキャストしている部分をRustでも再現するかは悩みどころですが、いったんは心を無にしてRustでも同様の処理を書きます。

#[no_mangle]
pub extern fn multiply_point(m: Matrix4x4, v: Vector3) -> Vector3 {
    let x = ((m.m00 as f64 * v.x as f64 + m.m01 as f64 * v.y as f64 + m.m02 as f64 * v.z as f64) + m.m03 as f64) as f32;
    let y = ((m.m10 as f64 * v.x as f64 + m.m11 as f64 * v.y as f64 + m.m12 as f64 * v.z as f64) + m.m13 as f64) as f32;
    let z = ((m.m20 as f64 * v.x as f64 + m.m21 as f64 * v.y as f64 + m.m22 as f64 * v.z as f64) + m.m23 as f64) as f32;
    let num = (1.0 / (m.m30 as f64 * v.x as f64 + m.m31 as f64 * v.y as f64 + m.m32 as f64 * v.z as f64) + m.m33 as f64) as f32;
    Vector3 { x: (x * num), y: (y * num), z: (z * num) }
}
テストコード
var v3 = UnityEngine.Random.insideUnitSphere;
Anchor = new GameObject().transform;
Anchor.position = UnityEngine.Random.insideUnitSphere;
Anchor.rotation = UnityEngine.Random.rotation;
Anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = Anchor.transform.localToWorldMatrix;

var withU = matrix.MultiplyPoint(v3);
Debug.Log(withU.ToStringFloat());

var withR = multiply_point(matrix, v3);
Debug.Log(withR.ToStringFloat());

実行結果
(-0.02336239, 0.5018276, 0.7009525)
(-Infinity, Infinity, Infinity)

Infinity...

数回繰り返したところ正負は合っているので、キャストに失敗して無限の彼方に辿り着いているようです。
こんなもんの原因追求する気はさらさらないのでRustのコードをきれいに書き直します。

fn multiple_float(a: f32, b: f32) -> f64 {
    ((a as f64) * (b as f64))
}

#[no_mangle]
pub extern fn multiply_point(m: Matrix4x4, v: Vector3) -> Vector3 {
    let x = multiple_float(m.m00, v.x) + multiple_float(m.m01, v.y) + multiple_float(m.m02, v.z) + m.m03 as f64;
    let y = multiple_float(m.m10, v.x) + multiple_float(m.m11, v.y) + multiple_float(m.m12, v.z) + m.m13 as f64;
    let z = multiple_float(m.m20, v.x) + multiple_float(m.m21, v.y) + multiple_float(m.m22, v.z) + m.m23 as f64;
    let a = multiple_float(m.m30, v.x) + multiple_float(m.m31, v.y) + multiple_float(m.m32, v.z) + m.m33 as f64;
    let num = 1.0 / a;
    Vector3 { x: (x * num) as f32, y: (y * num) as f32, z: (z * num) as f32 }
}

なんかもっときれいに書けるような、そうでもないような。
ともあれこれを実行してみます。

実行結果
(-0.1820646, -0.6444009, 0.6140736)
(-0.1820646, -0.6444009, 0.6140736)

一致しました。これでようやく完成です。

実験

さっそくC#と比べて早いのか遅いのか実験してみます。

テストコード
private const int PointCount = 1000000;

private async void Check(CancellationToken token)
{
    var anchor = new GameObject().transform;

    while (true)
    {
        anchor.position = UnityEngine.Random.insideUnitSphere;
        anchor.rotation = UnityEngine.Random.rotation;
        anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;

        var matrix = anchor.transform.localToWorldMatrix;
        var randomVectors = await Task.Run(() => GenerateRandomVectorAsync(_cancellationTokenSource.Token, PointCount), token);

        // Rustによる変換
        Profiler.BeginSample("#ByRust");
        var r = TransByRust(matrix, randomVectors);
        Profiler.EndSample();

        // Rust(Releaseビルド)による変換
        Profiler.BeginSample("#ByRustR");
        var rr = TransByRustRelease(matrix, randomVectors);
        Profiler.EndSample();

        // C#による変換
        Profiler.BeginSample("#ByCSharp");
        var c = TransByCsharp(matrix, randomVectors);
        Profiler.EndSample();

        Debug.Log(r.Length + " - " + rr.Length + " - " + c.Length);
    }
}

// C#による変換
private static Vector3[] TransByCsharp(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = matrix.MultiplyPoint(points[count]);
    }

    return ret;
}

// RustDebugビルドによる変換
private static Vector3[] TransByRust(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = multiply_point(matrix, points[count]);
    }

    return ret;
}

// RustReleaseビルドによる変換
private static Vector3[] TransByRustRelease(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = multiply_point_r(matrix, points[count]);
    }

    return ret;
}

// ランダムなVector3の配列を生成
private static Task<Vector3[]> GenerateRandomVectorAsync(CancellationToken cancellationToken, int length)
{
    var random = new System.Random();
    var points = new Vector3[length];
    for (var count = 0; count < points.Length; count++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        // UnityEngine.RandomのAPIはメインスレッドからしか呼べない...
        // なので無理矢理ランダムなVector3を生成する
        points[count].x = (float) (random.NextDouble() * random.Next(-100, 100));
        points[count].y = (float) (random.NextDouble() * random.Next(-100, 100));
        points[count].z = (float) (random.NextDouble() * random.Next(-100, 100));
    }

    return Task.FromResult(points);
}

これがプロファイラの結果です。

スクリーンショット 2019-11-27 3.56.35.png

C#が一番速い……。
Rustのリリースビルドとデバッグビルドで差が出ている以上、NativePluginだからプロファイラがおかしくなっているわけでもないようです。

f32が溢れるようなことはまずないので、Rust側でキャストを止めてみます。

multiply_point_without_cast
#[no_mangle]
pub extern fn multiply_point_without_cast(m: Matrix4x4, v: Vector3) -> Vector3 {
    let x = m.m00 * v.x + m.m01 * v.y + m.m02 * v.z + m.m03;
    let y = m.m10 * v.x + m.m11 * v.y + m.m12 * v.z + m.m13;
    let z = m.m20 * v.x + m.m21 * v.y + m.m22 * v.z + m.m23;
    let a = 1.0 / (m.m30 * v.x + m.m31 * v.y + m.m32 * v.z + m.m33);
    Vector3 { x: (x * a), y: (y * a), z: (z * a) }
}

スクリーンショット 2019-11-29 3.33.15.png

ちょっとはやくなってる。

仮説

1. UnityのMatrix4x4.MultiplyPointはC++層で実行されている
デコンパイルするってことはコンパイルされてるんだよねこれ
2. C#がmatrix4x4をキャッシュしているのに対し、Rustは毎回受け渡しているため非効率
これは確実にあるはず
3. 言語間で受け渡すコスト > Rustによる恩恵
単純な計算処理では意味がなかった

仮説1が一番大きいと思います。わたしが戦っていたのはキャストしまくりのC#ではなくバチバチにチューニングされたC++だったのです……たぶん。なので自分で実装した重い処理とかだったら違う結果が出るかもしれません。

仮説2の解決としてVector3[]の配列を受け渡しできればいいのですが、ポインタがわからないからマーシャリングもわからないので諦めました。Rust側でポインタを復元する方法もわからないです。

仮説3もわりとありそうな気がしています。いちいち変換している分のコストはかなり大きい……はず。

あと、いくらVector3とはいえこの数なら結構なGCを誘発していると思うのですが、プロファイラのGC Allocはみんないっしょです。NativePlugin部分に対するプロファイラの動作もいまいち情報がないのでよくわからん。

まとめ

Rust

今更ですがedition2018です。
Rustの学習ですが、ヤバいと噂の所有権は自分はそんなにひっかかりませんでした。でもライフタイムは微妙にまだよくわかってないかもしれない。
あと、エラー処理と並列プログラミングはちらっと読んだだけで何言ってるかまったく理解してないので改めて読もうと思います。
Rustの学習コストは確かに高いですが、コンパイラさんが徹底的にチェックしてくれることで実行時に吹っ飛ぶのを防止してくれるのはとても好きです。

NativePlugin

Unity+C#+Rustの知識を要求されるのつらい。

敗北

プログラミングぢからは高まった気がしますが、結果が出せていません。
しかし現在の自分ではこれ以上は手が出ない……。ポインタを理解するためにCを諦めてやってみるべきか……。
なにはともあれ今回はここで敗北します。誰かなんか強い人がなんとかしてくれたら嬉しいな! サヨナラ!

とりあえず書いた分は置いておきます。
gist

11
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
10