ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

【C#】【リフレクション】任意の型のインスタンスを文字列から生成する

MyClassというクラスがあったとします。
普通、MyClassインスタンスを生成したければこうです。

MyClass instance = new MyClass();

さて、C#は型がガッチリ決まっているのが素晴らしいところだとは思うのですが、それが困るケースもやはりあります。
他の言語で作ったJsonからデシリアライズしたいとか、実行時に生成したい型が初めてわかる、みたいなことはよくありますね。 作りたいインスタンスの型とそこに入れるJsonを両方文字列で指定できたらとても便利です。 そういった動的に変化する型に対応する際に使えるのがリフレクションです。

文字列からインスタンスを生成する

MyClassを文字列から生成したければこのように記述します。

object instance = System.Activate.CreateInstance(Type.GetType("MyClass"));

引数が文字列ですので、例えばテキストを読み込んで生成すれば色んな型がこれ一つで生成できてしまいます。
が、これは万能ではありません。 そもそもActivate.CreateInstanceというのはnewと書き方が違うだけでやっていることは同じです。 つまり上の記述は実質以下と同じ。

object instance = new MyClass();

ここでもしMyClassに引数付きのコンストラクタが宣言されていればどうでしょうか。
その場合引数無しのコンストラクタは自分で宣言していなければMyClassにはありませんね。 なのに無理やりリフレクションでnew MyClass()を呼んでしまうと、
MissingMethodExceptionという例外を吐いてしまいます。
これは読んでその通り、そんなメソッドは存在しないという例外です。 ありもしない引数無しコンストラクタを無理やり呼んでしまったようです。

引数無しのコンストラクタが無いクラスの場合

例えばMyClassがint型1つを引数に取るコンストラクタを持っているならば、

object instance = System.Activate.CreateInstance(Type.GetType("MyClass"), 1);

と書けばnew MyClass(1)と同じです。今回は問題なくインスタンスを生成できます。
が、これはMyClassを知っている場合だからできること。実行時に初めて型がわかるような場合に使いたいのがリフレクションなので、コンストラクタの引数もこんな形で指定できるはずがありません。

コンストラクタの引数を知る

ここからはusing System.Reflection;を記述してください。 MyClassに設定されたコンストラクタを知りたい場合。

ConstructorInfo[] constructors = Type.GetType("MyClass").GetConstructors();

が使えます。
これはConstructorInfoの配列を返すのでどれを使えばいいのか悩みますが、今回はインスタンス生成後にJsonやらなんやらを用いて値を代入していくので、どのコンストラクタを使おうが関係ありません。 とりあえずIndex = 0のやつを使いましょうか。
なお、コンストラクタを実行したいときはInvokeです。
引数にはとりあえずobjectの配列を渡しましょう。

object instance = constructors[0].Invoke(object[]);

渡すときの変数はobjectの配列にしますが、中身は何でもいいわけではないです。
intの引数が欲しいのに実体がstringなobjectを渡してしまったら実行時エラーですよね。
ではコンストラクタの引数はどうやって知るのかというと。

ParameterInfo[] parameters = constructors[0].GetParameters();

ParameterInfoは引数に関して色々な情報を持っていますが、今知りたいのはその型です。

Type[] arguTypes = parameters.Select(paramInfo => paramInfo.ParameterType).ToArray();

ParameterTypeがそれです。今回は引数の型一覧が欲しいのでLinqでも使いましょうか。

さあこれで、インスタンス生成の準備は整いました。

あらゆるクラスのインスタンスを文字列から生成する

以上をまとめると

  • インスタンス生成にはActivate.CreateInstance(Type)を用いる
  • 引数付きのコンストラクタを使う場合はConstructorInfo.Invoke(object[])

クラスの中にもクラスがあるのが普通ですから再帰的にする必要もあるでしょう。
以上のことを踏まえてこんなメソッドが書けました。

/// <summary>
/// typeのインスタンスを生成する
/// </summary>
/// <param name="type">生成したいインスタンスの型</param>
/// <returns></returns>
private static object createInstance(Type type)
{
    object obj;
    try {
        obj = Activator.CreateInstance(type);
        // 引数無しコンストラクタが無い場合は引数情報を引っ張ってきてConstructorInfoから生成する
    } catch(MissingMethodException) {
        var constructor = type.GetConstructors()[0];
        var parameters = constructor.GetParameters().Select(parInfo => createInstance(parInfo.ParameterType)).ToArray();
        obj = constructor.Invoke(parameters);
    }
    return obj;
} 

なかなかコンパクトに収まりましたね。

追記

上記のままだと特定のケースでエラーを吐いてしまいます(Char*にコンストラクタが無く怒られるっぽい…)。 コンストラクタが無いのは流石にどうにもできない……

対応としては、catchの中にさらにtry-catchを入れて、

try
{
    var constructor = type.GetConstructors()[0];
    var parameters = constructor.GetParameters().Select(parInfo => createInstance(parInfo.ParameterType)).ToArray();
    obj = constructor.Invoke(parameters);
}
    catch
{
    obj = null;
}

とすればChar*については問題なさそうですが、若干怪しい気もするので、defaultで切り抜けた方がいいかも。

public static object CreateInstance(Type type)
{
    var _createInstance = ((Func<int>)CreateInstance<int>).Method.GetGenericMethodDefinition();
    return _createInstance.MakeGenericMethod(type).Invoke(null, null);
}

public static T CreateInstance<T>()
{
    T obj = default;
    try
    {
        obj = Activator.CreateInstance<T>();
    }
    catch
    {
    try
        {
            var constructor = typeof(T).GetConstructors().[0];
            var parameters = constructor.GetParameters().Select(parInfo => CreateInstance(parInfo.ParameterType)).ToArray();
            obj = (T)constructor.Invoke(parameters);
        }
        catch { }
    }
    return obj;
}

RPGでよくある画面がパリーンって割れて戦闘シーンに入るやつのシェーダの解説

すごくジオメトリシェーダのサンプルっぽいものができました。 まずジオメトリシェーダの説明をします。 Wikipedia曰く、ジオメトリシェーダとは

頂点シェーダーの後に実行され、プリミティブ全体または隣接したプリミティブの情報を持つプリミティブを入力する。例えばトライアングルを処理するとき、3つの頂点がジオメトリシェーダーの入力となる。ジオメトリシェーダーはラスタライズされるプリミティブを出力でき、そのフラグメントは最終的にピクセルシェーダーに渡される。またプリミティブを出力せずにキャンセルすることもできる。

ものだそうです。

サンプルを見てみる

頂点シェーダが各頂点1つずつ処理を行うのに対して、 ジオメトリシェーダは 1. ポイント、ライン、プリミティブ単位で入力を行い、 2. 入力数と異なった出力数を設定できる という強みがあります。が、モバイルでは動きません。
ジオメトリシェーダの使えない環境では最初から複数のバラバラのポリゴンとして用意して動かす必要があります。

「プリミティブ単位の操作」の実例として「RPGの戦闘シーン遷移時でよくある画面が割れるやつ」を作っていきます。 戦闘シーンっぽいものとかを作っていないのでシーン遷移した感じが皆無ですが、まぁ雰囲気はわかるのではないかと……

f:id:u_osusi:20190330010452g:plain

尚、先述したようにジオメトリシェーダはプリミティブの出力数を増やすこともできるのですが、今回は初めから多数のプリミティブで構成された平面を用意しました。

f:id:u_osusi:20190325030728p:plain (雑にカットされたPlane)
多分、割るところから動的にやるなら「ドロネー分割」というアルゴリズムを利用するのが妥当なのではないかと思います。

Shader "Custom/FinalFantasyX"
{
    Properties
    {
        _MainTex("MainTex", 2D) = white {]
        distortedTime("distortedTime", float) = 0
    }
    SubShader
    {
        Cull Off
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag

            struct appdata
            {
                float2 uv : TEXCOORD0;
                float4 vertex : POSITION;
            };

            struct g2f
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            // 疑似乱数
            float rand(float2 st)
            {
                return frac(sin(dot(st, float2(12.9898, 78.233))) * 43758.5453);
            }

            // 回転行列をかける
            float4 rotate(float4 pos, float4 axis, float angle)
            {
                float4 nAxis = normalize(axis);
                return mul(float4x4(cos(angle) + pow(nAxis.x, 2) * (1 - cos(angle)), nAxis.x * nAxis.y * (1 - cos(angle)) - nAxis.z * sin(angle), nAxis.x * nAxis.z * (1 - cos(angle)) + nAxis.y * sin(angle), 0,
                                nAxis.y * nAxis.x * (1 - cos(angle)) + nAxis.z * sin(angle), cos(angle) + pow(nAxis.y, 2) * (1 - cos(angle)), nAxis.y * nAxis.z * (1 - cos(angle)) - nAxis.x * sin(angle), 0,
                                nAxis.z * nAxis.x * (1 - cos(angle)) - nAxis.y * sin(angle), nAxis.z * nAxis.y * (1 - cos(angle)) + nAxis.x * sin(angle), cos(angle) + pow(nAxis.z, 2) * (1 - cos(angle)), 0,
                                0, 0, 0, 1
                    ), pos);
            };

            float distortedTime;
            sampler2D _MainTex;

            appdata vert(appdata v)
            {
                return v;
            }

            [maxvertexcount(3)]
            void geom(triangle appdata input[3], inout TriangleStream<g2f> outStream)
            {
                float4 vertex0 = input[0].vertex;
                float4 vertex1 = input[1].vertex;
                float4 vertex2 = input[2].vertex;
                float3 norm = normalize(cross(vertex0.xyz - vertex1.xyz, vertex2.xyz - vertex0.xyz));
                float3 axis = normalize(rotate(vertex1 - vertex0, float4(norm, 0), rand(vertex0.xy) * 3.14159265358979 * 2));
                float4 center = (vertex0 + vertex1 + vertex2) / 3;
                float angle = rand(vertex0.xy) * distortedTime * 10;
                [unroll]
                for(int i = 0; i < 3; i++)
                {
                    float4 rotatedVert = center * ((distortedTime / 3) + 1 + pow(distortedTime, 2)) + rotate(input[i].vertex - center, float4(axis, 0), angle);
                    g2f o;
                    o.vertex = UnityObjectToClipPos(rotatedVert);
                    o.uv = v.uv;
                    outStream.Append(o);
                }

                outStream.RestartStrip();
            }

            float4 frag(g2f i) : SV_Target
            {
                float4 color = tex2D(_MainTex, i.uv);
                return half(color.xyz, color.w * exp(-distortedTime));
            }
            ENDCG
        }
    }
}

ジオメトリシェーダの解説

[maxvertexcount(3)]
void geom(triangle appdata input[3], inout TriangleStream<g2f> outStream)
{
    float4 vertex0 = input[0].vertex;
    float4 vertex1 = input[1].vertex;
    float4 vertex2 = input[2].vertex;
    float3 norm = normalize(cross(vertex0.xyz - vertex1.xyz, vertex2.xyz - vertex0.xyz));
    float3 axis = normalize(rotate(vertex1 - vertex0, float4(norm, 0), rand(vertex0.xy) * 3.14159265358979 * 2));
    float4 center = (vertex0 + vertex1 + vertex2) / 3;
    float angle = rand(vertex0.xy) * distortedTime * 10;
    [unroll]
    for(int i = 0; i < 3; i++)
    {
        float4 rotatedVert = center * ((distortedTime / 3) + 1 + pow(distortedTime, 2)) + rotate(input[i].vertex - center, float4(axis, 0), angle);
        g2f o;
        o.vertex = UnityObjectToClipPos(rotatedVert);
        o.uv = v.uv;
        outStream.Append(o);
    }

    outStream.RestartStrip();
}

こいつを1つ1つ見ていきます。

Attribute

[maxvertexcount(3)]

これは、1回の入力に対して最大で頂点数3個までしか出力しませんという宣言です。必須です。 尚、出力頂点数には上限があり、1024/要素数より大きくはできません。 今回出力構造体は計float6つなので、1024/6 =170.66...より大きくはできません。

引数、戻り値

void geom(triangle appdata input[3], inout TriangleStream<g2f> outStream)

triangle単位で、appdata型の要素数3の配列を入力、g2f型の配列をtriangleのストリームとして出力する、と言っています。 入出力プリミティブはtriangleには限らず、いくつか用意されていますが、今回は割愛します。(参考リンク先に記述あります)

真ん中の算数

今、各トライアングルにやってほしい動きは 1. 回転する 2. 初めの長方形の中心点から遠ざかっていく の2つです。 回転軸は最初の長方形平面に平行だといい感じな気がするので、そうなるようにランダムに軸を設定します。

float3 axis = normalize(rotate(vertex1 - vertex0, float4(norm, 0), rand(vertex0.xy) * 3.14159265358979 * 2));

トライアングルの一辺を、法線を軸にランダムな角度回転させたものを回転軸としています。

rotatedVertが、指定時間における各頂点のオブジェクト内ローカル座標となります。 それをMVP変換して、座標いじりは終わり。 これで動きは完成です。

    outStream.Append(o);
}

outStream.RestartStrip();

Appendで、出力として登録、 RestartStripで、今登録されてる頂点群を一つのプリミティブとみなす宣言です。 3つ送られるたびにまとめられるイメージです。

あとは、直前までカメラが写していた映像を貼り付けてやりましょう。

カメラの映像を貼り付ける

C#スクリプトで記述できます。 Texture2D.ReadPixelsというメソッドがあり、非常に名前がわかりづらいのですが、これがスクリーンのピクセルをテクスチャにコピーするメソッドです。ということでこれで終わりですね。

終わり

RPGの戦闘シーン遷移時でよくある画面が割れるやつ」に必要なものが揃いました。
ここに載せたシェーダ以外には、遷移時に誤魔化しのように全体にブラーをかけるポストエフェクトを適用しています。

さて、以上のことを以下の手順で実行すればOK。

1. テクスチャを割れるやつ貼り付け、割れるやつをアクティブに(or 生成)する。
2. 遷移前シーンを削除する。
3. 割れるシェーダアニメーションを開始する。
4. Mainカメラにブラーをかける。
5. 遷移先シーンをアクティブにする。
6. Mainカメラのブラーを弱めていく。

さて、改めて完成したものを見てみましょう。 f:id:u_osusi:20190330010452g:plain

シェーダをいじり始めて3か月ほど、ジオメトリシェーダは初めて触りました。
まだかなり初心者っぽいですが、一応はちゃんと動いて整えれば使えそうなものができて楽しいですね。

参考

最終的に作ったものが違う以外完全上位互換な記事↓とてもわかりやすいです。 http://edom18.hateblo.jp/entry/2018/07/11/140455

RPGでよくある画面がパリーンって割れて戦闘シーンに入るやつのシェーダ

シェーダで実装できたー! f:id:u_osusi:20190312000256g:plain

この動画は単なるPlaneを割ってるだけですが、画面遷移時にRenderTextureを使ってPlaneのテクスチャとしてその瞬間を張り付けて割ってやれば完成なんじゃないかと思います。

今度コード上げます。
人様に見せられるコードにしてからね。

シェーダのお勉強、基礎まとめ

Unityでのシェーダを勉強中なので、備忘録も含めて学んだことのメモです。

シェーダは何から勉強したらいいのかわからないとよく言われ、事実僕もちょっと前まで何の知識も無かったので、そこからどうやって勉強したのかとか、初心者目線で書ければいいかなと思います。

そもそもシェーダって何してるの? という疑問を解消するには、レンダリングパイプラインというものについて調べるのが最も正しいのではないかと思います。レンダリングパイプラインの説明は調べればそれはもう色々と出てきますが、この辺とか最初に触れる部分を特に丁寧に説明していて良いのではないでしょうか。

yttm-work.jp

実際にUnityでシェーダを書いてみる

とりあえずシェーダを書いてみましょう。
完璧に理解するよりも先にいじって動かすのがプログラミング学習です。
あ、用いているUnityのバージョンはUnity2018.3.5f1です。

まず、シェーダファイルを作成します。
プロジェクトタブで
Create -> Shader -> Standerd Surface Shader f:id:u_osusi:20190310184013p:plain

名前はTestとでもしましょうか。

このような中身のシェーダファイルが生成されました。

Shader "Custom/Test"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

なんだこれという感じですね。
これはライティングも意識した場合のシェーダですが、最初からこんなのいじるのは良くないので、 とりあえず上のデフォルトスクリプトのことは完全に忘れて、以下のように書き直してください。

Shader "Custom/Test"
{
    Properties
    {
    }
    SubShader
    {
        Pass{
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                float2 uv : TEXCOORD0;
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.uv = v.uv;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                return fixed4(i.uv.x, i.uv.y, 0, 0);
            }
            ENDCG
        }
    }
}

超シンプル、というわけではないですが、かなりマシになりましたね。 実はシェーダ作成の時点でUnlitシェーダやImageEffectシェーダを選んでいると似た感じのデフォルトスクリプトが生成されます。
なんかUnityはsurfaceシェーダをお勧めしてくるんですが、ちゃんと理解せずに使うものではないと思います……
さて、これをQuadで実行すると f:id:u_osusi:20190311002929p:plain このようになります。
QuadはHierarchy上で右クリック -> 3D Object -> Quadで作成できます。 シェーダはそのままでは利用できず、Materialというものを作ってあげる必要があります。 Projectタブの、先ほど作成したTestシェーダの上で右クリック -> Create -> MaterialとするとTestシェーダのMaterialが作成されます。 作成したMaterialをQuadまでドラッグすると(C#スクリプトのときと同じように)完了。 上図にようになるはずです。
いくつかのシェーダ入門記事を見ていて確信したのですが、この赤緑のシェーダは
シェーダ界におけるHello World.
です。

このHello World.の解説はまた次の記事で行います。
お読みいただいてありがとうございました。

シェーダのお勉強

シェーダを触ってみたかったので簡単に勉強してみました。

その過程でとても参考になったページがこちら

3Dメイン

nn-hokuson.hatenablog.com

2Dメイン

docs.google.com

シェーダは見た目のお話なので、やっぱり実際に動かしてみるのが楽しい。

これらは実サンプルが豊富ですが、基本の知識がちゃんとそろっている資料も必要。

私はこちらを参考にしました。

booth.pm

サンプルは一切なく、全くの初心者がこれを読んでもシェーダを書けるようになるわけではないのですが、他の資料でシェーダコードを見た際に間違いなく理解度が上がります。 辞書のようなものですね。

で、本当はなんか作ってみて上げたかったのですが……サンプルをいじる程度しかできなかった…… 本気でやろうと思うと3Dモデリングも必要っぽい HoudiniとかBlenderとか使ってみるかなぁ 全然知識ないけど

UnityのVR開発に手を付けたら脱線した

さて、僕はUnityを仕事で使っているのである程度扱うことができます。 さらにSteamでVRゲームをするためにHTC Viveを持っています。

となれば自分でもちょっと作ってみたいなぁと思うのは必至。

SteamVR Pluginを導入し

assetstore.unity.com

とりあえずVR用カメラをシーン内に配置。 それだけでVR空間ができてしまいました。 しかし、このままではゲーム実行中にUnityエディタが見れないことに気付きました。
ヘッドセットを被ってるので当然ですね。

で、PC画面をキャプチャしてUnityのシーン上にレンダリングさせればいいかと思って、もう誰かが作ってそうだなとも思ったので探してみると案の定。 凹みさんという方が作成し、公開してくださっていました。

tips.hecomi.com

これでスムーズにVR空間をいじれるぞ、と思ったのですが、

VR空間に佇む巨大なスクリーンを見て思ってしまったのです。

これでアニメ観たいなぁ……
結果、ただPC画面をVR空間に投影するだけのアプリをビルド。お粗末ですがVRシアターを堪能し、VR開発は全然進まないのでした。

Unityでアプリの容量を小さくする設定の罠

UnityのBuildSettingを見るとCompressionMethodというものがあります。 f:id:u_osusi:20190124224608p:plain:w300

これはアプリに含まれるリソースを圧縮してくれる設定で、配信できるサイズが制限されているスマートフォン用アプリだとありがたい設定、のように思います。

が、実はここには罠があって、 iOS用のApp Storeで配信されるipaファイルとAndroid用のGoogle Playで配信されるapkファイルは既にzip形式で圧縮されたものなので、CompressionMethodでLZ4やLZ4HCに設定したところで大して容量削減効果はありません。LZ4は伸長スピードがウリの圧縮方法であって圧縮率はそれほどでもなく、むしろ増えるまであります。
ただ、iOSの場合はインストールした時点でipaファイルの解凍が行われるので、その段階まで行くと圧縮設定の効果がしっかり発揮されます。
さらに、App Storeで確認できるアプリ情報の容量は解凍後の容量なので、ぱっと見軽いアプリっぽく見せることもできますね。

よって、残念ながらCompressionMethodはスマートフォン環境で配信時のアプリ容量削減には活用できません。