前書き
この記事は、2023のUnityアドカレの12/21の記事です。
今年は、完走賞に挑戦してみたいと思います。Qiita君ぬい欲しい!
はじめに
C#における構造体(アンマネージドな)は、バイナリレベルでの配置情報が固定されています。
[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct Value
{
byte field0
int field1;
byte field2;
}
// Marshal.SizeOf(typeof(Value)) => 6 byte
//
// |<-field0->|<-----------------field1------------------>|<-field0->|
// | 00 | 01 | 02 | 03 | 04 | 05 |
故に、ファイルからメタデータをメモリに持ってきて、構造体にキャストしたりできます。
byte[] mem = File.ReadAllByte(path)[0..sizeof(Value)];
ref Value value = ref Unsafe.As<byte, Value>(ref mem[0]);
Console.WriteLine(value.field0);
Console.WriteLine(value.field1);
Console.WriteLine(value.field2);
コンパイル時にバイナリのフォーマットの中身がわからない場合
上記のように、バイナリのフォーマットが確定していない場合がありえます。byte配列として、状態を切り替えながらパース指定くのでもよいのですが、それではとても非効率です。
TypeBuilderなどの実行時にAssemblyを作成する方法もあります。
しかし今回は、これは使わずに、ジェネリックを駆使してやってみたいと思います。3DCGでMeshの頂点バッファを扱う場合を想定して、頂点同士をブレンドできるようにLerp可能な構造体を作ります。
ベースになる構造体を定義
考え方としては、二分木構造のようにします。
[StructLayout(LayoutKind.Sequential, Pack = 1)]
readonly struct Flex<T, U> where T : unmanaged where U : unmanaged
{
public readonly T Value;
public readonly U Next;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Flex(T value, U next) => (Value, Next) = (value, next);
}
ここにLerpを追加します。Lerpには加算と乗算も必要なので、IAdditionOperators
とIMultiplyOperators
も実装します。
interface ILerpable<T> where T : unmanaged
{
public T Lerp(in T b, float s, in T c, float t);
}
readonly struct Flex<T, U> : ILerpable<Flex<T, U>>,
IAdditionOperators<Flex<T, U>, Flex<T, U>, Flex<T, U>>,
IMultiplyOperators<Flex<T, U>, float, Flex<T, U>>
where T : unmanaged, IAdditionOperators<T, T, T>, IMultiplyOperators<T, float, T>
where U : unmanaged, IAdditionOperators<U, U, U>, IMultiplyOperators<U, float, U>
{
...
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Flex<T, U> Lerp(in Flex<T, U> b, float s, in Flex<T, U> c, float t)
=> new (Value + b.Value * s + c.Value * t, Next + b.Next * s + c.Next * t);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Flex<T, U> operator +(Flex<T, U> left, Flex<T, U> right)
=> new(left.Value + right.Value, left.Next + right.Next);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Flex<T, U> operator *(Flex<T, U> left, float weight)
=> new(left.Value * weight, left.Next * weight);
}
使い方
まずはイメージを持つために、静的にジェネリックパラメータを渡す場合を見てみましょう。
Flex<
float, // Position.x
Flex<
float, // Position.y
Flex<
float, // Position.z
Flex<
float, // UV.x
float // UV.y
>
>
>
> vertex0, vertex1, vertex2;
vertex0 = new(0, new(0, new(0, new(0, 0))));
vertex1 = new(1, new(0, new(0, new(1, 0))));
vertex2 = new(0, new(1, new(0, new(0, 1))));
var vertex_012 = Lerp(vertex0, vertex1, 0.5f, vertex2, 0.5f);
Console.WriteLine($"Size of {vertex_012.GetType()} is {Marshal.SizeOf(vertex_012)}\n[{vertex_012}]");
// -> [0.5, 0.5, 0, 0.5, 0.5]
static T Lerp<T>(T a, T b, float s, T c, float t) where T : unmanaged, ILerpable<T>
=> a.Lerp(b, s, c, t);
動的、ランタイムでジェネリックパラメータを渡すには、MakeGenericType
で構造体の型を作ります。そして受け入れるメソッドはILerpable
を受け付けるジェネリックメソッドにしておき、MakeGenericMethod
で構造体用に具体化します。
var typeList = new List<Type>()
{
typeof(float), typeof(float), typeof(float),
typeof(float), typeof(float),
};
Type vertexType = typeList.AsEnumerable()
.Reverse()
.Skip(2)
.Aggregate(
typeof(Flex<,>).MakeGenericType(typeList[^2], typeList[^1]),
(acc, t) => typeof(Flex<,>).MakeGenericType(t, acc));
var vertex_012 = typeof(Modern).GetMethod(nameof(Lerp), BindingFlags.Static | BindingFlags.NonPublic)!
.MakeGenericMethod(vertexType)
.Invoke(null, [vertex0, vertex1, 0.5f, vertex2, 0.5f, ]);
Console.WriteLine($"Size of {vertex_012!.GetType()} is {Marshal.SizeOf(vertex_012)}\n[{vertex_012}]");
// -> [0.5, 0.5, 0, 0.5, 0.5]
static T Lerp<T>(T a, T b, float s, T c, float t) where T : unmanaged, ILerpable<T>
=> a.Lerp(b, s, c, t);
GenericMathが使えない場合
.NET7以前…(UnityやUnityなど)ではGenericMath使えません。また、System.Numerics
のベクトル型もなぜかGenericMathに対応していません。代わりに型スイッチを使うことで実現できます。
[StructLayout(LayoutKind.Sequential, Pack = 1)]
readonly struct Flex<T, U> : ILerpable<Flex<T, U>>
where T : unmanaged
where U : unmanaged
{
...
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Flex<T, U> Get() => this;
public Flex<T, U> Lerp(in Flex<T, U> b, float s, in Flex<T, U> c, float t)
{
var value = Lerp(Value, b.Value, s, c.Value, t);
var next = Lerp(Next, b.Next, s, c.Next, t);
return new(value, next);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static unsafe V Lerp<V>(V a, V b, float s, V c, float t) where V : unmanaged
{
V result;
switch((a, b, c))
{
case (ILerpable<V> a_l, ILerpable<V> b_l, ILerpable<V> c_l):
result = a_l.Lerp(b_l.Get(), s, c_l.Get(), t);
break;
case (float a_32, float b_32, float c_32):
var r32 = a_32 + b_32 * s + c_32 * t;
result = *(V*)&r32;
break;
case (double a_64, double b_64, double c_64):
var r64 = a_64 + b_64 * s + c_64 * t;
result = *(V*)&r64;
break;
case (Vector2 a_v2, Vector2 b_v2, Vector2 c_v2):
var r_v2 = a_v2 + b_v2 * s + c_v2 * t;
result = *(V*)&r_v2;
break;
case (Vector3 a_v3, Vector3 b_v3, Vector3 c_v3):
var r_v3 = a_v3 + b_v3 * s + c_v3 * t;
result = *(V*)&r_v3;
break;
case (Vector4 a_v4, Vector4 b_v4, Vector4 c_v4):
var r_v4 = a_v4 + b_v4 * s + c_v4 * t;
result = *(V*)&r_v4;
break;
default:
Throw<V>();
result = default;
break;
}
return result;
}
static void Throw<V>() => throw new NotSupportedException($"{typeof(V)} is not supported");
}
}
まとめ
TypeBuilder縛りの、ジェネリックツリーで可変長構造体的なことを実現しました。byte配列で引き回す方法と比べるとこの後構造体を使う側で分岐などをしなくて済むというメリットがあります。TypeBuilderを使う方法と合わせて。この方法がどれぐらい優位性があるかというのは中々評価に悩ましいところですね。
プロジェクトコード全体