神様は有休消化中です。

Unity関連の技術ネタを書いてます。Graphics得意にしているのでその辺の話が多めです。

【Unity】uGUI ImageでAlpha Maskを使えるように実装する方法

はじめに

この記事は、先日公開した以下のリポジトリの技術解説です。
使ってみたいという方は、ぜひ使ってみてください。
github.com

概要

Unity uGUIでMask表現を行う場合、標準で実装されているMaskコンポーネントを使用すると、境界にひどいジャギーがでることがあります。
f:id:appleorbit:20180728183724p:plain

Unity標準のMaskコンポーネントはステンシルでクリップを行うため、斜めのラインにエイリアスが出てしまうことが原因です。
Mask画像にアルファ値を仕込んでも、ステンシルに書き込まれる際に描く or 描かないの2値情報になってしまうためです。

高解像度の環境であればあまり問題にならないのですが、低解像度環境であったり、モバイルのようにパフォーマンス上の問題で解像度を下げる場合に問題が表面化します。
マスクのアルファ値によって、マスク対象のアルファ値を操作することができれば、マスク画像の境界にアルファフェードをかけることでこのジャギーを目立たないようにすることができそうです。

実装上のハードル

実現したいことは「uGUI-Maskと同じインターフェースでAlpha Maskを実現したい」です。
しかし、これが結構面倒臭い。
マスクとマスク対象が1対1の関係で、座標が絶対に動かないのであればシェーダーで楽に実装できますが、1対多の関係に加えてマスクもマスク対象も自由に動くとなると途端に難しくなります。例えば下図のような場合です。
f:id:appleorbit:20180728185920p:plain
この場合、求める動作は「赤丸とマスクの白い部分が被っているピクセルだけ、画面に表示される(黄色い丸は表示されない)」となります。

考え方

マスク対象の頂点を描画する際に、その頂点位置のマスクUVを求めることができれば、UI/Defaultを改造した以下のようなシェーダーでAlpha Maskを実現できそうです。

つまり、何らかの方法でマスク対象の頂点座標を、マスクのUV座標に変換してやる必要があります。
f:id:appleorbit:20180728201228p:plain

実装方法

画面内のマスクのみを映すVP行列を作る*1

uGUI-Imageのポリゴンは必ず四角形なので、その四角形をスクリーンと見立てたVP行列を作成できれば、頂点座標から特定ポリゴンのUVを求めることができます。
マスクのImageコンポーネントでタイリングやスライスは使えなくなりますが、諦めることにします。

Unityでは、Matrix4x4.Ortho関数を使って、Orthograohicなプロジェクション行列を生成することができます。
docs.unity3d.com

この関数の引数に渡すleftなどは、以下の座標系での値です。*2
f:id:appleorbit:20180728204453p:plain

この座標系で、マスクの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の方が境界線が綺麗です。
f:id:appleorbit:20180728205432p:plain

【Unity】Inspectorで管理できる汎用アセットインポーターを公開しました

仕事で所属しているプロジェクトが佳境に入りまして、テクスチャの最適化などをアーティストさんと共同で進めております。
その中で「この非圧縮テクスチャを圧縮に変えたいのに変わらない!」「テクスチャのmaxSizeを512にしたいのにできない!」というような叫び声がよく聞こえるわけです。AssetPostProcessorのせいですね、はい。
開発初期〜中期であればゲームを作るだけなので放り込めばそれなりに動くインポーターでも問題ないのですが、佳境に入るとパフォーマンス対策のために細かく設定したくなります。
もっと欲をいえば、アーティストさん側にインポーターの設定管理までお願いしたい・・・というわけで、GUIで設定できる汎用のアセットインポーターを作って公開しました。

github.com

作った経緯

そもそもAssetPostProcessorってアーティストさんに優しくないよね?と常々思っておりました。プログラムを読まないと何しているかわからないし、どのフォルダに効いているのかもわからない。
Art「このファイルの設定が変わらなくて・・・」
Eng「ああ、このフォルダ以下はインポーターが設定してるから・・・」
Art「そうなんですか。ではこのフォルダだけ除外してください」
とか非効率この上ないと思います。

Unity-Asset-Importer-Extension

このアセットは、フォルダのインスペクターからアセットのインポート設定が行えます。
親フォルダの設定を子フォルダが引き継ぐため、親フォルダ側で大まかな設定(例:UI-Textureフォルダ以下のテクスチャはすべてSpriteにする等)を行い、子フォルダ側で細かい設定(例:圧縮するか、mipmap作るか等)を行うことができます。
f:id:appleorbit:20180725213129p:plain
もちろん、親フォルダの設定を子フォルダで無効化することも可能です。無効化した場合は、無効化した子フォルダ以下のフォルダには適用されなくなります。

デフォルトで、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がインポートしないからインポート順に悩まなくて済むとか、XMLシリアライズするとUnityバージョン考えなくて良いからポータビリティ高いよねとか、ベース部分の設計は私の師匠の書いたコードから引継ぎました。大変勉強になりました。

【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なるレンダリングパスが増えてしまいます。

f:id:appleorbit:20171103050838p:plain

これは公式ページにも記載されている通り、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を取得することができました。
カラーバッファを画面に書き戻すコストは増えていますが、ドローコールを重複して発行するコストに比べれば安いとおもいます。
f:id:appleorbit:20171103055104p:plain

【Unity】【uGUI】リストビューに、セルが画面外から差し込まれるアニメーションをつける

uGUIで作ったリストビューに、セルが画面外から差し込まれるようなアニメーションを追加する方法を共有。
最終的にこんな感じになります。
f:id:appleorbit:20160402223716g:plain

続きを読む

【Unity】カメラからどれだけ離れても、最低1ピクセルは表示されるように保障する

通常3Dオブジェクトがカメラから離れすぎると、レンダリングサイズが1ピクセルを下回った時点で画面に描画されなくなり、カメラとの位置関係によって描画が切れたり出たりしてチカチカと表示されることがあります。今回はこの問題の解決方法(レンダリングの最低ピクセル保障)を共有します。

続きを読む