【Unity】Reflectionを使った情報収集を軽量化するアイデア
ライブラリを開発している中で、書き心地を担保するためにReflectionを使用したい場面が出てきます。
例えば特定のキーが押された時に指定した関数が実行されるようなショートカットキーの仕組みを実装する場合、Attributeでショートカットアクションを指定出来るようになっていると便利です。
[Shortcut("ファイル保存", KeyCode.LeftControl, KeyCode.S)] static void OnSave() { // 保存処理 }
このような作りにしようとした場合、ライブラリの初期化処理で以下のようなコードを書いてショートカットアクションを収集することになります。
void CollectShortcutActions() { // アプリ内の全てのアセンブリを取得 foreach(var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { // すべての型を取得 foreach(var type in assembly.GetTypes()) { // 型の中からprivate static, public static関数を取得 foreach(var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { // [Shortcut]が指定されているかを判定 var attr = method.GetCustomAttributes<ShortcutAttribute>(); if (attr == null) continue; // ショートカットアクションの登録処理 } } } }
見るからに重そうですね。
Reflectionを使った処理はもちろんオーバーヘッドの高いものですが、アプリ内の全てのAssemblyに対して処理を行っているため、Unityエンジンの持つクラス群やこのAttributeを使っていないAssemblyに対しても検索処理が行われ、かなりの処理時間がかかってしまっています。
今回はこの処理をなるべく軽くするためのアイデアを共有します。
AttributeのAssemblyを分ける
まず初めに、収集したいAttributeクラスを別Assemblyへ切り分けます。
UnityにはAssembly Definition Assetという仕組みがあり、指定したフォルダ以下のコードを別のAssemblyへ切り分けることができます。
Assembly Definition Assetについては、公式マニュアルやこちらのページを参照してください。
対象のAssemblyが、AttributeのAssemblyに依存しているかどうかをチェックする
当然ですが、Attributeを使用しているクラスが含まれるAssemblyは、Attributeが定義されたAssemblyを参照しています。
例えばShortcutAttributeを収集したいと考えたときは、ShortcutAttributeを定義したAssemblyを参照しているものだけを検索すればよいことになります。
C#では、AssemblyがどのAssemblyを参照しているかを取得するAPIが用意されています。
これを使って、ショートカットアクションの収集処理を以下のように書き換えます。
void CollectShortcutActions() { foreach(var method in GetAllMethodWithAttribute<ShortcutAttribute>()) { // ショートカットアクションの登録処理 } } IEnumerator<MethodInfo> GetAllMethodWithAttribute<T>() where T : System.Attribute { var targetAssemblyName = typeof(T).Assembly.GetName().FullName; foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { var referencedAssemblies = assembly.GetReferencedAssemblies(); var isTarget = assembly.GetName().FullName == targetAssemblyName; if (!isTarget) { for (int i = 0; i < referencedAssemblies.Length; i++) { var assemblyInfo = referencedAssemblies[i]; if (assemblyInfo.FullName == targetAssemblyName) { isTarget = true; break; } } } if (!isTarget) continue; foreach (var type in assembly.GetTypes()) { foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) { var attr = method.GetCustomAttributes<T>(); if (attr == null) continue; yield return method; } } } }
これで、Unityエンジンのクラス群や関係のないAssemblyへの検索が無くなったことにより、実行速度が大幅に改善します。
【Unity】uGUI ImageでAlpha Maskを使えるように実装する方法
はじめに
この記事は、先日公開した以下のリポジトリの技術解説です。
使ってみたいという方は、ぜひ使ってみてください。
github.com
概要
Unity uGUIでMask表現を行う場合、標準で実装されているMaskコンポーネントを使用すると、境界にひどいジャギーがでることがあります。
Unity標準のMaskコンポーネントはステンシルでクリップを行うため、斜めのラインにエイリアスが出てしまうことが原因です。
Mask画像にアルファ値を仕込んでも、ステンシルに書き込まれる際に描く or 描かないの2値情報になってしまうためです。
高解像度の環境であればあまり問題にならないのですが、低解像度環境であったり、モバイルのようにパフォーマンス上の問題で解像度を下げる場合に問題が表面化します。
マスクのアルファ値によって、マスク対象のアルファ値を操作することができれば、マスク画像の境界にアルファフェードをかけることでこのジャギーを目立たないようにすることができそうです。
実装上のハードル
実現したいことは「uGUI-Maskと同じインターフェースでAlpha Maskを実現したい」です。
しかし、これが結構面倒臭い。
マスクとマスク対象が1対1の関係で、座標が絶対に動かないのであればシェーダーで楽に実装できますが、1対多の関係に加えてマスクもマスク対象も自由に動くとなると途端に難しくなります。例えば下図のような場合です。
この場合、求める動作は「赤丸とマスクの白い部分が被っているピクセルだけ、画面に表示される(黄色い丸は表示されない)」となります。
考え方
マスク対象の頂点を描画する際に、その頂点位置のマスクUVを求めることができれば、UI/Defaultを改造した以下のようなシェーダーでAlpha Maskを実現できそうです。
つまり、何らかの方法でマスク対象の頂点座標を、マスクのUV座標に変換してやる必要があります。
実装方法
画面内のマスクのみを映すVP行列を作る*1
uGUI-Imageのポリゴンは必ず四角形なので、その四角形をスクリーンと見立てたVP行列を作成できれば、頂点座標から特定ポリゴンのUVを求めることができます。
マスクのImageコンポーネントでタイリングやスライスは使えなくなりますが、諦めることにします。
Unityでは、Matrix4x4.Ortho関数を使って、Orthograohicなプロジェクション行列を生成することができます。
docs.unity3d.com
この関数の引数に渡すleftなどは、以下の座標系での値です。*2
この座標系で、マスクのuGUI-Imageの矩形を定義します。
具体的にはuGUI-Imageの4角のワールド座標を取得してスクリーン座標に変換した後、正規化してcamera.orthographicSizeの座標系に変換します。
ここまでできれば後は簡単で、VP行列を生成してマテリアルにSetMatrixしてやればCPU側の処理は完了です。
TRS行列をかけているのは、-1〜1の範囲で取得した値を0〜1の範囲に変換するためです。
対応する頂点シェーダーで、先ほど作成したVP行列からUV座標を計算する処理は以下です。
結果
これで、uGUI-ImageでAlpha Maskを使えるようになりました。
Unity標準のMaskよりも、Alpha Maskの方が境界線が綺麗です。
【Unity】Inspectorで管理できる汎用アセットインポーターを公開しました
仕事で所属しているプロジェクトが佳境に入りまして、テクスチャの最適化などをアーティストさんと共同で進めております。
その中で「この非圧縮テクスチャを圧縮に変えたいのに変わらない!」「テクスチャのmaxSizeを512にしたいのにできない!」というような叫び声がよく聞こえるわけです。AssetPostProcessorのせいですね、はい。
開発初期〜中期であればゲームを作るだけなので放り込めばそれなりに動くインポーターでも問題ないのですが、佳境に入るとパフォーマンス対策のために細かく設定したくなります。
もっと欲をいえば、アーティストさん側にインポーターの設定管理までお願いしたい・・・というわけで、GUIで設定できる汎用のアセットインポーターを作って公開しました。
作った経緯
そもそもAssetPostProcessorってアーティストさんに優しくないよね?と常々思っておりました。プログラムを読まないと何しているかわからないし、どのフォルダに効いているのかもわからない。
Art「このファイルの設定が変わらなくて・・・」
Eng「ああ、このフォルダ以下はインポーターが設定してるから・・・」
Art「そうなんですか。ではこのフォルダだけ除外してください」
とか非効率この上ないと思います。
Unity-Asset-Importer-Extension
このアセットは、フォルダのインスペクターからアセットのインポート設定が行えます。
親フォルダの設定を子フォルダが引き継ぐため、親フォルダ側で大まかな設定(例:UI-Textureフォルダ以下のテクスチャはすべてSpriteにする等)を行い、子フォルダ側で細かい設定(例:圧縮するか、mipmap作るか等)を行うことができます。
もちろん、親フォルダの設定を子フォルダで無効化することも可能です。無効化した場合は、無効化した子フォルダ以下のフォルダには適用されなくなります。
デフォルトで、Unity2017.1f1で定義されているAssetImporterに設定を流し込む仕組みを提供しています。
DefaultTextureImporterなどがそれです。
これらのデフォルトインポーターは自動生成しており、生成するツールも同梱しているので、Unityのバージョンが合わない人はREADMEに書いた手順で出力し直してもらえればと思います。
いちおし機能
このアセットのいちおしは、独自のインポーターもGUIで設定できるようになることです。
public class CustomTextureImporter : IAssetImporterExtension { private bool isHogeFiag; public System.Type GetTargetImporterType() { return typeof(TextureImporter); } public void Apply (AssetImporter originalImporter, string assetPath, Property[] properties) { bool hogeFlag = properties.Where(o => o.name == "isHogeFlag").Select(o=>bool.Parse(o.value)).First(); // hogeFlagを使った処理 } }
のようなクラスを定義すれば、フォルダのインスペクターからインポーターの適用・isHogeFlagの設定が行えるようになります。
現状だとAssetPostProcessorのOnPreprocess〜にしか対応していないのですが、OnPostprocess〜にも対応すればアセットコピーやAssetBundleNameの自動設定なんかもこのアセットで実装できるようになります。
-
-
- 追記 ---
-
簡単に対応できそうだったんでOnPostprocess対応しました。
OnPostprocessAllAssetsのタイミングで、IAssetImporterExtension.OnPostprocessとOnRemoveprocessをアセットごとに呼び出します。
サンプルとして、CustomImporter/以下にCopyAssetImporterを実装しています。
これで、テクスチャを特定フォルダに入れればサイズの違うテクスチャを自動生成とかできるようになりました。
最後に
作ったばかりなのでまだ使いにくいところなどあるとは思いますが、使ってみてフィードバックやらissueやらいただけると大変励みになります。
【Unity】iPhoneX対応の罠 なぜかCanvasのAnchorがずれる問題の解決策
概要
遅ればせながら、今開発中の新作ゲームでiPhoneX対応を行うことになりました。
ネットを検索するといろんな情報が溢れていますが、まずは公式情報を・・・ということでUnity公式のサンプルを元に対応を進めた結果、見事に地雷を踏んだので共有しておきます。
Unity公式情報
iPhoneXの画面問題は業界的にもかなり迷惑なものでした。
Unityもいち早くこの問題に向き合い、パッチリリースを経て正式にSafeAreaに対応しています。
helpdesk.unity3d.co.jp
Unity - Scripting API: Screen.safeArea
一つ目のリンクからUnityのサンプルをみることができます。
そのサンプルの内容は、要約すると以下の通りです。
void ApplySafeArea()
{
var area = Screen.safeArea;
var anchorMin = area.position;
var anchorMax = area.position + area.size;
anchorMin.x /= Screen.width;
anchorMin.y /= Screen.height;
anchorMax.x /= Screen.width;
anchorMax.y /= Screen.height;
panel.anchorMin = anchorMin;
panel.anchorMax = anchorMax;
}
サンプルの罠
上記のサンプルですが、特定の状況下で不具合を引き起こします。
私が遭遇した不具合は、以下のようなものです。
- CanvasのAnchorが右上にずれる
- 一部のUIが明滅を繰り返すようになる
原因
上記に書いた不具合は、Screen.SetResolutionで画面解像度を変更している場合に発生します。
原因は、anchorMin/anchorMaxの各要素をScreen.width/heightで正規化している部分です。
Screen.width/heightは、SetResolution以降は指定した解像度を返すようになります。
しかし、Screen.safeAreaで返される値はディスプレイの解像度のため、正規化の計算が狂い、CanvasのAnchorが右上にずれてしまうわけです。
対応
デバイスのディスプレイ解像度をDisplayクラスから取得し、その値で正規化することで問題を解決することができます。
void ApplySafeArea() { var area = Screen.safeArea; var display = Display.displays[0]; var screenSize = new Vector2Int(display.systemWidth, display.systemHeght); var anchorMin = area.position; var anchorMax = area.position + area.size; anchorMin.x /= screenSize.x; anchorMin.y /= screenSize.y; anchorMax.x /= screenSize.x; anchorMax.y /= screenSize.y; panel.anchorMin = anchorMin; panel.anchorMax = anchorMax; }
【Unity】カメラ1つでUI解像度を維持し、3D解像度だけを下げる方法
昔からモバイル端末の解像度は狂気の沙汰としか言えず、あの小さな面積にフルHDとかのディスプレイを積んでいます。
撮った写真を綺麗に見るなら良いのかもしれませんが、1フレーム16msとか33msで描画しないといけないゲームではかなり辛いものがあります。
正直、Androidの解像度合戦がなければ、もっとシェーダーリッチなゲームが世の中に溢れていると思うわけです。
とはいえ世の流れですので、我々エンジニアは工夫を凝らしてよりリッチな見た目が出せるよう日々邁進するわけであります。
上記を踏まえて言いたいことは、
ゲームの解像度落としても良いよね?
ということです。
しかし、UI解像度を720pまで落としてしまうと途端にユーザーにバレてしまいます。
UIは結構解像度の劣化が目立つんですよね。
しかし、3D空間側の解像度を落としてもほとんど目立たず、フィルレートが稼げて美味しいのです。
私の尊敬するエンジニアの1人、Unityの安原さんもUnity 2017 Tokyoで同じことをおっしゃっています。
【Unite 2017 Tokyo】スマートフォンでどこまでできる?3Dゲームをぐりぐり動かすテクニック講座
3D側の解像度だけを下げる
さて、やっと本題です。
やりたいことは、「UI解像度は高く」「3D解像度は低く」レンダリングです。
簡単に言うと3D描画を自前で用意したRenderTextureに描画して、3D描画が終わったらBack BufferにBlitすれば良いことになります。
Google先生に聞いてみると、以下のような記事がヒットしました。
wordpress.notargs.com
この記事の中では、Back BufferにBlitする用のカメラを用意して、そのカメラにCommandBufferをAddすることでRenderTextureをBackBufferに書き込んでいます。
これは、同一カメラでのCommandBufferではAfterImageEffects, AfterEveryThingでのSetRenderTargetでBack Bufferを指定出来ないからだと認識していました。事実私もこの記事を参考にいろいろ試したのでですが指定出来ず、以前の記事ではこの方式で実装しています。
appleorbit.hatenablog.com
しかし、しかしです!
本当に偶然ではあるのですが、以下の記事をみつけました。
Unity 5: AfterImageEffects and AfterEverything Traps – Updated My Journal.
全CameraEvent共通で、SetRenderTarget( -1 )と指定するとBack Bufferがセットされる(RenderTargetIdentifer( int nameID )でそうなっている?)
本当にこのブログの著者様には感謝の念しかありません。
というわけで、書いてみました。
以下のコードで、3D解像度だけを1つのカメラで下げることができます。
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; /// <summary> /// 解像度を変更するコンポーネント /// </summary> [RequireComponent(typeof(Camera))] public class ResolutionConverter : MonoBehaviour { /// <summary> /// 解像度 /// </summary> public enum Resolution { None, HD, FULLHD } /// <summary> /// 解像度設定 /// </summary> [SerializeField] private Resolution m_Resolution = Resolution.None; /// <summary> /// ターゲットカメラ /// </summary> private Camera m_Camera; /// <summary> /// フレームバッファ /// </summary> private RenderTexture m_FrameBuffer; /// <summary> /// コマンドバッファ /// </summary> private CommandBuffer m_CommandBuffer; /// <summary> /// 初期化 /// </summary> private void Awake() { m_Camera = GetComponent<Camera> (); Apply (m_Resolution); } /// <summary> /// 適用する /// </summary> public void Apply(Resolution resolution) { m_Resolution = resolution; if (resolution == Resolution.None) { return; } var size = GetResolutionSize (resolution); size = FitScreenAspect (Screen.width, Screen.height, size.x, size.y); UpdateFrameBuffer (size.x, size.y, 24); UpdateCameraTarget (); AddCommand (); } /// <summary> /// フレームバッファの更新 /// </summary> private void UpdateFrameBuffer(int width, int height, int depth, RenderTextureFormat format = RenderTextureFormat.Default) { if (m_FrameBuffer != null) { m_FrameBuffer.Release (); Destroy (m_FrameBuffer); } m_FrameBuffer = new RenderTexture (width, height, depth, format); m_FrameBuffer.useMipMap = false; m_FrameBuffer.Create (); } /// <summary> /// カメラの描画先を更新 /// </summary> private void UpdateCameraTarget() { if (m_FrameBuffer != null) { m_Camera.SetTargetBuffers (m_FrameBuffer.colorBuffer, m_FrameBuffer.depthBuffer); } else { m_Camera.SetTargetBuffers (Display.main.colorBuffer, Display.main.depthBuffer); } } /// <summary> /// 解像度の実サイズを取得 /// </summary> private Vector2Int GetResolutionSize(Resolution resolution) { bool isPortrait = Screen.height > Screen.width; switch(resolution){ case Resolution.HD: if (isPortrait) { return new Vector2Int (720, 1280); } return new Vector2Int (1280, 720); case Resolution.FULLHD: if (isPortrait) { return new Vector2Int (1080, 1920); } return new Vector2Int (1920, 1080); } return new Vector2Int (Screen.width, Screen.height); } /// <summary> /// 解像度の計算 /// </summary> private static Vector2Int FitScreenAspect(int width, int height, int maxWidth, int maxHeight) { // 解像度以下なら何もしない if (width <= maxWidth && height <= maxHeight) { return new Vector2Int (width, height); } if (width > height) { float aspect = height / (float)width; int w = Mathf.Min (width, maxWidth); int h = Mathf.RoundToInt (w * aspect); return new Vector2Int (w, h); } { float aspect = width / (float)height; int h = Mathf.Min (height, maxHeight); int w = Mathf.RoundToInt (height * aspect); return new Vector2Int (w, h); } } /// <summary> /// コマンドを追加する /// </summary> private void AddCommand() { RemoveCommand (); // カラーバッファをバックバッファ(画面)に描きこむコマンド { m_CommandBuffer = new CommandBuffer (); m_CommandBuffer.name = "blit to Back buffer"; m_CommandBuffer.SetRenderTarget (-1); m_CommandBuffer.Blit (m_FrameBuffer, BuiltinRenderTextureType.CurrentActive); m_Camera.AddCommandBuffer (CameraEvent.AfterEverything, m_CommandBuffer); } } /// <summary> /// コマンドを破棄する /// </summary> private void RemoveCommand() { if (m_CommandBuffer == null) { return; } if (m_Camera == null) { return; } m_Camera.RemoveCommandBuffer (CameraEvent.AfterEverything, m_CommandBuffer); m_CommandBuffer = null; } }
個人的に最近最もテンションの上がった内容でした。
【Unity】無駄なドローコールなしで深度バッファを取得する方法
UnityでDepth Bufferを使用する方法として公式に紹介されているのは、Camera.depthTextureModeを使用する方法です。
しかし、Camera.depthTextureModeを使用すると、UpdateDepthTextureなるレンダリングパスが増えてしまいます。
これは公式ページにも記載されている通り、Unityの仕様のようです。
https://docs.unity3d.com/jp/540/Manual/SL-CameraDepthTexture.html
デプステクスチャは、シャドウキャスターのレンダリングに使用するのと同じシェーダー内パスを使用してレンダリングされます(“ShadowCaster” pass type)。
Unity内部で、Shader Replaceを使ってDepth Bufferを書くためだけにドローコールを発行しているためにこの挙動となっています。
理由は不明ですが、おそらくOpenGL2.0世代のモバイル端末はNative Depth Textureをサポートしていない端末があったので、それらに対応するためなのかなと。
ハイエンド系ならMRTで自前で深度バッファ書くだろ的な。
Unityの仕様はどうあれ、Depth Bufferのためにドローコールを発行するとなると描画負荷に無視できないインパクトが出てきます。
そこで、パスを増やさずにDepth Bufferを取得する方法を模索しました。
Depth Bufferを取得する
結論から言うと、Camera.SetTargetBuffersを使用します。
private Camera m_Camera; private RenderTexture m_ColorBuffer; private RenderTexture m_DepthBuffer; private void Start() { m_Camera = GetComponent<Camera> (); // |私は深度バッファを取得するためにドローコールを発行しました><| //m_Camera.depthTextureMode = DepthTextureMode.Depth; // カラーバッファを生成 m_ColorBuffer = new RenderTexture (Screen.width, Screen.height, 0); m_ColorBuffer.Create (); // 深度バッファを生成 m_DepthBuffer = new RenderTexture (Screen.width, Screen.height, 24, RenderTextureFormat.Depth); m_DepthBuffer.Create (); m_Camera.SetTargetBuffers (m_ColorBuffer.colorBuffer, m_DepthBuffer.depthBuffer); }
このままだとShaderから使用できないので、CommandBufferを使って任意のタイミングでセットします。
加えて、カメラの描画先がカラーバッファになってしまっているので、Back buffer(画面)に書き戻します。
private void AddCommand() { // 深度バッファをセットするコマンド { CommandBuffer command = new CommandBuffer (); command.name = "Set depth texture"; command.SetGlobalTexture ("_DepthTexture", m_DepthBuffer); m_Camera.AddCommandBuffer (CameraEvent.BeforeImageEffects, command); } // カラーバッファをバックバッファ(画面)に描きこむコマンド { CommandBuffer command = new CommandBuffer (); command.name = "blit to Back buffer"; // (注) // カメラの書き込み先がRenderTextureなのに、CameraEvent.AfterEverythingのタイミングで // CameraTargetがback bufferを示すのは正しいのだろうか・・・ // 確認バージョン:Unity5.6.1f1 command.SetRenderTarget (BuiltinRenderTextureType.CameraTarget); command.Blit (m_ColorBuffer, BuiltinRenderTextureType.CurrentActive); m_Camera.AddCommandBuffer (CameraEvent.AfterEverything, command); } }
これで、_DepthTextureにShaderからアクセスすることが可能になりました。
こんな感じのShaderで、画面に深度バッファが表示できます。
Shader "Test/BlitDepth" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float2 uv_depth : TEXCOORD1; float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; o.uv_depth = v.uv; return o; } sampler2D _MainTex; sampler2D _DepthTexture; fixed4 frag (v2f i) : SV_Target { half rawDepth = SAMPLE_DEPTH_TEXTURE(_DepthTexture, i.uv_depth); half depth = Linear01Depth(rawDepth); return fixed4(rawDepth, rawDepth, rawDepth, 1); } ENDCG } } }
UpdateDepthTextureパスなしでDepth bufferを取得することができました。
カラーバッファを画面に書き戻すコストは増えていますが、ドローコールを重複して発行するコストに比べれば安いとおもいます。
【Unity】【uGUI】リストビューに、セルが画面外から差し込まれるアニメーションをつける
uGUIで作ったリストビューに、セルが画面外から差し込まれるようなアニメーションを追加する方法を共有。
最終的にこんな感じになります。