のりまき日記

Unityなどの活用リファレンスブログ。「こうしたい時どうする」をまとめたい

unity)uGUIの角丸マスクをマスク画像なしで作りたい

はじめに

uGUIにもマスクの機能がありますが、曲線を含むマスクを掛けようと思うとジャグジャギして使えません。マスク用画像でマスクするシェーダーを自作することが多いと思いますが、今回はパラメータで角丸の比率を変えられるシェーダーを作ってみようと思います。

こうだ!

memo!

uGUIのマスクがジャギるのは、たぶんアルファクリップなのでアンチエイリアスが効かないからだと思います。

雛形のシェーダーを入手

unityのダウンロードサイトからビルトインシェーダーのセットをダウンロードします。UI-Default.shaderを探してプロジェクトにコピーします。

Download Archive

ここから

実装方法を考える

rounded corner shaderというキーワードで検索してみると以下のサイトが見つかりました。

Shader - Shadertoy BETA

以下の部分が肝みたいですが、パッと見でなにをしているかさっぱりわかりません。

return length(max(abs(CenterPosition)-Size+Radius,0.0))-Radius;

考察する

自分ならどう実装するかを考えながらソースを眺めていたらわかりました。かんたんに解説します。

解説しよう!

  • 例えば150pxの画像を20pxの半径で角丸にしたい時
  • 四隅から20px小さくした角からの距離を見て
  • xy共にプラス方向で20pxより離れてれば抜く

という計算になります。

シェーダを改造

上記のような計算をシェーダーで処理を書くときは、極力シンプルかつ制御構文を使わず計算だけで実現する方法を考えます。頂点シェーダー内なら多少if分があってもいいと思いますが、ピクセルシェーダー内で行う計算なので最適化を考えていきます。

Shader "UI/Rounded"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0

        _Round ("角丸", Range(0,1)) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend One OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                float4  mask : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;
            float _UIMaskSoftnessX;
            float _UIMaskSoftnessY;
            float _Round;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                float4 vPosition = UnityObjectToClipPos(v.vertex);
                OUT.worldPosition = v.vertex;
                OUT.vertex = vPosition;

                float2 pixelSize = vPosition.w;
                pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));

                float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
                float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
                OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
                OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));

                OUT.color = v.color * _Color;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);

                #ifdef UNITY_UI_CLIP_RECT
                    half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
                    color.a *= m.x * m.y;
                #endif

                // uv座標(0~1)から(-1,1)の座標に変換
                float2 pos = IN.texcoord * 2 - 1;

                // 4角を意識しなくていいように全て右上の座標(0~1)にする
                float2 fixedPos = abs(pos);

                // 半径分小さくした角の座標(半径は割合)
                float2 corner = 1 - _Round;

                // 小さくした角からの位置
                float2 localPos = fixedPos - corner;
                
                // マイナスになる座標は0にして距離を計算(角から見て右上方向以外は半径の長さを超えなくなる)
                float distance = length(max(localPos, 0));

                // 半径より離れているか
                float fixedDistance = distance - _Round;

                // これだとややジャギル
                //color.a = 1 - step(0, fixedDistance);
                
                // これがアンチエイリアスっぽくなる
                color.a = 1 - smoothstep(0, .01, fixedDistance);

                #ifdef UNITY_UI_ALPHACLIP
                    clip (color.a - 0.001);
                #endif

                color.rgb *= color.a;

                return color;
            }
            ENDCG
        }
    }
}

note!

// uv座標(0~1)から(-1,1)の座標に変換
float2 pos = IN.texcoord * 2 - 1;

// 4角を意識しなくていいように全て右上の座標(0~1)にする
float2 fixedPos = abs(pos);

// 半径分小さくした角の座標(半径は割合)
float2 corner = 1 - _Round;

// 小さくした角からの位置
float2 localPos = fixedPos - corner;

// マイナスになる座標は0にして距離を計算(角から見て右上方向以外は半径の長さを超えなくなる)
float distance = length(max(localPos, 0));

// 半径より離れているか
float fixedDistance = distance - _Round;

// これだとややジャギル
//color.a = 1 - step(0, fixedDistance);

// これがアンチエイリアスっぽくなる
color.a = 1 - smoothstep(0, .01, fixedDistance);

今回追記した部分です。先ほどの考察結果を式にしました。smoothstepをつかってアンチエイリアス感を出しているのがポイントです。

完成!

おわりに

サンプル画像にLennaを使いたかったのですが、本人が使わないで欲しいと言っているようなのでマンドリルを使いました。

memo!

ja.wikipedia.org 画像圧縮とLennaについて知っていると面白いです。

www.ess.ic.kanagawa-it.ac.jp マンドリルは↑から頂きました。

unity)PlayerとかEnemyのクラス設計をきれいにしたい

はじめに

  • Playerクラスを作らずにプレーヤーを表現したい!という設計の話です
  • すべてがそれで上手くいくわけではないですが、使える場面は多いと思います
  • だいたい毎回うまくできないので、いつかうまく設計できるように考え方をメモしておきます

unityはコンポーネント思考

  • UnityはGameObjectに色々なコンポーネントをアタッチして動きを作ります
    • 部品を組み合わせて、ロボットを作る様なイメージです
  • 細かい粒度では、位置を決定するtransformがあって、描画するためにrendererがあって、物理を制御するrigidbodyがあって、という感じ

characterクラスに集約

  • transformやらrendererやらrigidbodyを制御するクラスはどうしても必要なので、それをCharacterと命名します
  • CharacterにはMoverとかAttackerとかいい感じの粒度でコンポーネントを実装します
  • PlayerもEnemyもCharacterです

Playerとは

  • Playerっぽい動きをするCharacterがPlayerです
    • なのでGameObjectのnameがPlayerであるべきだと思います
  • Playerっぽい動きかどうかを決めるのはController
    • ユーザーが操作するならUserControllerのアタッチされたCharacterがPlayerという名前のGameObject

Enemyとは

  • AiControllerのアタッチされたCharacterが、ターゲットとしてPlayerを指定されたら、そいつはEnemyという名前のGameObject

良い点

  • ごっこのゲームを作っていたら「鬼も操作できるようにして!」と言われた
    • EnemyとPlayerで実装してると大変だった
  • Controllerをすげ替えるだけでPlayerにもEnemyにもなれるがよい

マリオ

  • マリオを作るときにmarioクラスが登場するのは具体的過ぎると感じます
    • プログラムはいつだって抽象的で汎用的あったほうがいいと思う
      • 1〜10を出力するのにたぶんforを使うと思います。print 1; print2;...とはしないのと同じ感じ

結局Playerクラスを作ってしまう

  • なんだかんだ「これはPlayer!」「これはEnemy!」と書いちゃった方がラクなことが多い
  • 継承したいこともやっぱりある

銀の弾丸はない

  • 作りたいものによって最適な設計は変わると思う
  • でも意識して作れると疎結合できれいなクラス分けくらいの設計になるのでおすすめ

unity)uGUIっぽいUIの書き方をしたい

はじめに

uGUIを使っているとインスペクタやスクリプトから値を変更するだけで、ビューも更新されて気持ちいいです。これはたぶんリアクティブです。

こういう動き

GUIアプリ

上記はGUIアプリケーションでは普通の挙動だと思います。プロパティを変更するとイベントが発行されてビューが更新される仕組みです。Flashで開発をおこなっていた時はこういう実装がかんたんに出来ていた記憶があります。またgetterやsetterについての面白い記事がありました。

www.eisbahn.jp

iGUI

OnGUIGUILayoutなどを使って作るuiです。こちらはスクリプトで部品を定義して毎フレーム描画する方式です(最適化されているので実際の挙動は違います)。こちらはこちらで使い勝手がいいので、僕の場合デバッグ用uiなんかはこっちを使うことが多いです。

実装する

僕が普段unityでuiコンポーネントを作る時に使っているコードを載せてみます。

UIBehaviour

ベースとなるクラスを作成します。

using UnityEngine;

public abstract class UIBehaviour : MonoBehaviour
{
    bool m_IsDirty;

    protected virtual void LateUpdate()
    {
        CheckDirty();
    }

#if UNITY_EDITOR
    // インスペクタで値を変えた時用
    protected virtual void OnValidate()
    {
        m_IsDirty = true;
        CheckDirty();
    }
#endif

    protected virtual void Render()
    {
    }

    protected void Set<T>(ref T prop, T value)
    {
        if (!value.Equals(prop))
        {
            prop = value;
            m_IsDirty = true;

#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                CheckDirty();
            }
#endif
        }
    }

    protected void CheckDirty()
    {
        if (m_IsDirty)
        {
            Render();
            m_IsDirty = false;
        }
    }
}

note!

#if UNITY_EDITOR
    // インスペクタで値を変えた時用
    protected virtual void OnValidate()
    {
        m_IsDirty = true;
        CheckDirty();
    }
#endif

エディターでインスペクタで値を変えた時のイベント発行はOnValidateがやってくれます。どのプロパティを変更したかなどは取れませんが、エディターなのであまり気にしません。

protected void Set<T>(ref T prop, T value)
    {
        if (!value.Equals(prop))
        {
            prop = value;
            m_IsDirty = true;

#if UNITY_EDITOR
            if (!Application.isPlaying)
            {
                CheckDirty();
            }
#endif
        }
    }

    protected void CheckDirty()
    {
        if (m_IsDirty)
        {
            Render();
            m_IsDirty = false;
        }
    }

値の変更を監視して変更があればフレームのおわりに再描画します。1つのプロパティが変更されるだけで全体再描画になりますが、適切にコンポーネントが部品かされていれば、そんなにオーバーヘッドは気にならないと思います。

使い方

ラベルをインスペクタで設定できるボタンを作ってみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MyButton : UIBehaviour
{
    [SerializeField]
    string m_Label;
    public string label { get => m_Label; set=>Set(ref m_Label, value); }

    [SerializeField]
    Text m_LabelText;

    protected override void Render()
    {
        m_LabelText.text = m_Label;
    }
}

これでインスペクタから値を変更しても、スクリプトから値を変更してもビューが更新されます。

動作する様子

既存コンポーネントを使った使い方

InputFieldをラップしたコンポーネントを作ってみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MyInput : UIBehaviour
{
    [SerializeField]
    string m_Message;
    public string message { get => m_Message; set => Set(ref m_Message, value); }

    [SerializeField]
    InputField m_InputField;

    private void OnEnable()
    {
        m_InputField.onValueChanged.AddListener(OnValueChange);
    }

    private void OnDisable()
    {
        m_InputField.onValueChanged.RemoveListener(OnValueChange);
    }

    void OnValueChange(string value)
    {
        message = value;
    }

    protected override void Render()
    {
        m_InputField.text = m_Message;
    }
}

こんな感じになります。

note!

OnValueChangeした結果Renderが呼ばれてm_InputField.textに値を代入しているので、無限ループが発生しそうですがこれは問題ないです。下位のコンポーネント内で「値の変更があったらイベント発火」という挙動になっているので、ループは発生しません。上記で実装したUIBehaviourもそうなっています。

コンポーネントを組み合わせた使い方

「はい」と「いいえ」のボタンを持ったコンポーネントを作ってみます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class MyConfirmButton : UIBehaviour
{
    [SerializeField]
    string m_YesLabel;
    public string yesLabel { get => m_YesLabel; set => Set(ref m_YesLabel, value); }

    [SerializeField]
    string m_NoLabel;
    public string noLabel { get => m_NoLabel; set => Set(ref m_NoLabel, value); }

    [SerializeField]
    MyButton m_YesButton;

    [SerializeField]
    MyButton m_NoButton;

    protected override void Render()
    {
        m_YesButton.label = m_YesLabel;
        m_NoButton.label = m_YesLabel;
    }
}

note!

protected override void Render()
{
    m_YesButton.label = m_YesLabel;
    m_NoButton.label = m_YesLabel;
}

Renderが呼ばれる度に両方のボタンを再設定していますが、上記で書いた通り「値の変更があったらイベント発火」になってるので、そんなに無駄はないと思います。

unity内部の実装

uGUIはC#で書かれているので以下のリポジトリでソースを見ることができます。これをみるとだいたい同じ様なことをしているようです。あまり統一感がなくコンポーネントによっていろいろな書き方がされているので、読んでみると面白いです。 github.com

memo!

SetPropertyUtilityを作ったりGraphicを継承したして処理を共通化しているようなので、これを使いたかったのですが、internalなクラスだったりで使えませんでした。この辺を使えるとよりunityチックに書けていいなと思いました。

イベントに関して

イベントには大きくSystem.Actionを使う方法とUnityEventを使う方法があると思います。uiに関しては後者を使うべきだと思います。uGUIはエディターを使って作るものなので、インスペクタでイベントもバインドできるUnityEventを使うのが自然だと思うからです。

おわり

この辺の実装に関してはそれぞれの流儀があると思うので、こんな流儀もあるよ、という気持ちで記事にしました。unirxなんかを使ってもスマートに書けると思います。「プロパティを変更したらビューも変更したい」みたいな原始的な仕組みを、unityの実装を見ながら再実装できたのは勉強になりました。

unity)ディープリンク(URLスキーム)をeditorにも対応させたい

完成図

はじめに

unityはディープリンクにも対応していますが、エディターには対応していませんでした。デバッグする際にエディターでも確認できると便利なので実装してみます。

docs.unity3d.com

やること

ディープリンクはURLを使ってアプリを開く仕組みです。通常「myapp://scheme」みたいに指定するところを、「http://localhost/scheme」と指定してもURLなので動くはずです。今回はエディターにwebサーバーを立てることで独自にディープリンクを処理させてみます。

webサーバーに関しては以下の記事の実装を使います。

tsururin.hatenablog.com

ディープリンクのハンドリング

通常のハンドリングに加えて、webサーバーでもハンドリングできるようにします。

using System.Web;
using UnityEngine;

public class DeepLinkHandler : MonoBehaviour
{
    public const int EditorSchemePort = 3333;

    public event System.Action<string> onDeepLinkRequest;

#if UNITY_EDITOR
    Server m_Server;
#endif

    private void OnEnable()
    {
        // 通常の処理
        Application.deepLinkActivated += OnDeepLinkActivated;

#if UNITY_EDITOR
        // エディター用の処理
        m_Server = new Server(EditorSchemePort);
        m_Server.Route(new SchemeHandler(OnDeepLinkRequest));
        m_Server.Start();
#endif
    }

    private void OnDisable()
    {
        // 通常の処理
        Application.deepLinkActivated -= OnDeepLinkActivated;

#if UNITY_EDITOR
        // エディター用の処理
        m_Server.Stop();
#endif
    }

    void OnDeepLinkActivated(string url)
    {
        var uri = new System.Uri(url);
        var query = HttpUtility.ParseQueryString(uri.Query);
        var data = query.Get("data");
        OnDeepLinkRequest(data);
    }

    void OnDeepLinkRequest(string data)
    {
        onDeepLinkRequest?.Invoke(data);
    }
}
using System.Net;
using System.Text;
using System.Threading.Tasks;

public class SchemeHandler : IRequestHandler
{
    public string path => "/scheme";

    public string method => "GET";

    System.Action<string> m_Callback;

    public SchemeHandler(System.Action<string> callback)
    {
        m_Callback = callback;
    }

    public Task<bool> Handle(HttpListenerRequest req, HttpListenerResponse res)
    {
        var data = req.QueryString.Get("data");

        res.StatusCode = 200;
        var text = Encoding.UTF8.GetBytes("success! open unity editor.");
        res.OutputStream.Write(text, 0, text.Length);
        res.Close();

        m_Callback?.Invoke(data);

        return Task.FromResult(true);
    }
}

動かしてみる

unityからブラウザを起動して、ブラウザからunityに処理を戻してみます。ついでにdataも渡してみます。

server側

まずはwebページが必要なので、以下のようなindex.htmlを作ります。

<html>
    <head>
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
       <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   </head>
    <body>
        <h1>My Deep Link Test page</h1>
        <p><a id="link" href="">unityに戻る</a></p>

        <script>
           const searchParams = new URLSearchParams(window.location.search);
           let scheme = searchParams.get("scheme");
            scheme += "?data=ok";
           document.getElementById("link").setAttribute("href", scheme);
       </script>
    </body>
</html>

pythonで簡易サーバーを起動します。これでhttp://localhost:8080/で上記HTMLのページが表示されます。

% cd test
% python3 -m http.server --cgi 8080

note

const searchParams = new URLSearchParams(window.location.search);
let scheme = searchParams.get("scheme");
scheme += "?data=ok";

ブラウザがアプリ起動に使うURLは、unityから渡してあげるようにします。ブラウザ側ではエディターから呼ばれたのか、アプリから呼ばれたのかわからないためです。

unity側

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    [SerializeField]
    Text m_Message;

    [SerializeField]
    DeepLinkHandler m_DeepLinkHandler;

    void Start()
    {
        m_Message.text = "アイルビーバッグ";
    }

    private void OnEnable()
    {
        m_DeepLinkHandler.onDeepLinkRequest += OnDeepLinkRequest;
    }

    private void OnDisable()
    {
        m_DeepLinkHandler.onDeepLinkRequest -= OnDeepLinkRequest;
    }

    public void OnOpenURL()
    {
        var scheme = "myapp://scheme";
#if UNITY_EDITOR
        scheme = "http://localhost:3333/scheme";
#endif
        Application.OpenURL(string.Format("http://localhost:8080?scheme={0}", scheme));
    }

    void OnDeepLinkRequest(string data)
    {
        m_Message.text = string.Format("もどったぞ(data: {0})", data);
    }
}

これで動きました。

完成図

おわり

ブラウザからエディターを操作してる感じがして、なんだか面白いです。ほかにも活かせそうだと思いました。

unity)かんたんにwebサーバーを立てたい

はじめに

unityでインタラクションコンテンツを作っていると、アプリ間の連携などでhttpによる通信がしたいことがよくあります。よく使う実装をメモとして残しておきます。

tsururin.hatenablog.com

コード

using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using UnityEngine;

public class Server : System.IDisposable
{
    HttpListener m_HttpListener;

    List<IRequestHandler> m_RequestHandlers = new List<IRequestHandler>();

    public Server(int port)
    {
        m_HttpListener = new HttpListener();
        m_HttpListener.Prefixes.Add(string.Format("http://*:{0}/", port));
    }

    public void Start()
    {
        Debug.LogFormat("[Server] start.");
        m_HttpListener.Start();
        var _ = Listen();
    }

    public void Stop()
    {
        Debug.LogFormat("[Server] stop.");
        m_HttpListener.Stop();
    }

    public void Route(IRequestHandler handler)
    {
        m_RequestHandlers.Add(handler);
    }

    async Task Listen()
    {
        while (m_HttpListener.IsListening)
        {
            var context = await m_HttpListener.GetContextAsync();
            var _ = Routing(context);
        }
    }

    async Task Routing(HttpListenerContext context)
    {
        foreach (var handler in m_RequestHandlers)
        {
            if (context.Request.Url.AbsolutePath.StartsWith(handler.path) && context.Request.HttpMethod == handler.method)
            {
                Debug.LogFormat("[Server] routing. path: {0}, method: {1}", handler.path, handler.method);
                try
                {
                    var result = await handler.Handle(context.Request, context.Response);
                    if (result)
                    {
                        return;
                    }
                }
                catch (System.Exception e)
                {
                    Debug.LogException(e);
                    context.Response.StatusCode = 500;
                    context.Response.Close();
                }
            }
        }
        Debug.LogWarningFormat("[Server] 404.");
        context.Response.StatusCode = 404;
        context.Response.Close();
    }

    public void Dispose()
    {
        m_HttpListener.Close();
    }
}
using System.Net;
using System.Threading.Tasks;

public interface IRequestHandler
{
    string path { get; }

    string method { get; }

    Task<bool> Handle(HttpListenerRequest req, HttpListenerResponse res);
}

note

サーバのコードです。

List<IRequestHandler> m_RequestHandlers = new List<IRequestHandler>();

リクエスト毎にハンドラを作成して登録できるようにします。

Debug.LogException(e);
context.Response.StatusCode = 500;
context.Response.Close();

...

Debug.LogWarningFormat("[Server] 404.");
context.Response.StatusCode = 404;
context.Response.Close();

シンプルに500や404を実装しています。

使い方

using UnityEngine;

public class ApiServer : MonoBehaviour
{
    public System.Action onSomethingEvent;

    [SerializeField]
    int m_Port = 3000;

    Server m_Server;

    private void Awake()
    {
        m_Server = new Server(m_Port);
        m_Server.Route(new HelloWorldHandler());
        m_Server.Route(new WebEventHandler(onSomethingEvent));
    }

    private void OnEnable()
    {
        m_Server.Start();
    }

    private void OnDisable()
    {
        m_Server.Stop();
    }
}
using System.Net;
using System.Text;
using System.Threading.Tasks;

public class HelloWorldHandler : IRequestHandler
{
    public string path => "/hello-world";

    public string method => "GET";

    public async Task<bool> Handle(HttpListenerRequest req, HttpListenerResponse res)
    {
        // なにか処理
        await Task.Yield();

        res.StatusCode = 200;
        var text = Encoding.UTF8.GetBytes("hello world!");
        res.OutputStream.Write(text, 0, text.Length);
        res.Close();

        return true;
    }
}
using System.Net;
using System.Text;
using System.Threading.Tasks;

public class WebEventHandler : IRequestHandler
{
    public string path => "/my-event";

    public string method => "GET";

    System.Action m_Callback;

    public WebEventHandler(System.Action callback)
    {
        m_Callback = callback;
    }

    public async Task<bool> Handle(HttpListenerRequest req, HttpListenerResponse res)
    {
        // なにか処理
        await Task.Yield();

        res.StatusCode = 200;
        var text = Encoding.UTF8.GetBytes("ok!");
        res.OutputStream.Write(text, 0, text.Length);
        res.Close();

        m_Callback?.Invoke();

        return true;
    }
}

note

m_Server = new Server(m_Port);
m_Server.Route(new HelloWorldHandler());
m_Server.Route(new WebEventHandler(onSomethingEvent));

サーバを起動してハンドラを登録しています。ハンドラは単純にレスポンスを返すだけでもいいし、リクエストを受けてアプリ内に通知して挙動を変えるなどしてもいいと思います。

おわりに

外部にサーバーを立てたい時はapacheやnode.jsなどで立てるのがラクですが、アプリ間通信くらいでよければこちらも使えると思います。

unity)通信プロトコルの選び方

はじめに

unityでインタラクションコンテンツを作っていると、アプリ間の連携などでネットワーク通信がしたいことがよくあります。やりたいことによって最適なプロトコルがあるのでまとめてみます。なんとなく知ってるだけでも使えると思うので、細い説明を省いて超ざっくりまとめます。(細い説明はネットにたくさんあるので検索してください。)

よく使うプロトコル

  • HTTP
  • WebSocket
  • OSC

TCPUDP

コンテンツを作る上で知っておきたいのは以下になると思います。

プロトコル 使い所
TCP 相手に送るよ!とあいさつしてから返事があってから送る。確実に届いてほしい時に使う。丁寧だけど遅い。
UDP 勝手に送る。届いてる保証はない。だいたい届いてればいい時に使う。雑だけど速い。

note

プロトコルは以下に該当します

使い分け

僕が使い分ける時はだいたい以下を基準に選んでいます

プロトコル 使い所 具体例
HTTP リクエストしてレスポンスがほしい時 設定の同期など
WebSocket 双方向でデータを送りたい時 オンライン対戦など
OSC 一方的に命令したい時 アプリの制御など

よくやる実装

unityで使う場合いろいろ実装方法はありますが、僕は以下を利用しています。

プロトコル サーバー クライアント
HTTP C#で作る C#で作る
WebSocket 中間サーバーを置きたい時はnode.jsで作る websocket-sharpで作る
OSC uOSC uOSC

おわりに

リアルタイム通信をしたい時にPhotonなどのサービスを使うこともできますが、簡単な位置同期などであればWebSocketで自作してしまうと無料でお手軽にできたりします。

unity)Splatoonみたいにインクを塗るのを実装してみたい ~デプスシャドウ編~

塗られない様子

はじめに

前回の記事でいい感じにインクを塗ることができました。今回は「遮蔽されたオブジェクトの後ろはインクが塗られない」という機能を追加で実装してみました。

tsururin.hatenablog.com

やること

インクを光と考えると塗られない部分は影になります。プロジェクターにデプスシャドウを実装することで実現してみます。

デプスシャドウに関しては以下の記事がとても参考になりました。 shibuya24.info

プロジェクターからみたデプスを取得する

前回プロジェクターを自作した際に、視錐台計算用に描画しないカメラを使っていました。そのカメラを使って、プロジェクターから見たデプスをレンダーテクスチャに描画するようにします。

private void Start()
{
    ...
    m_Camera.depthTextureMode = DepthTextureMode.Depth;
    m_Camera.targetTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.Depth);
    m_Material.SetTexture("_DepthTex", m_Camera.targetTexture);
}

note

m_Camera.targetTexture = new RenderTexture(512, 512, 16, RenderTextureFormat.Depth);

デプスを書き込むテクスチャのフォーマットです。

m_Material.SetTexture("_DepthTex", m_Camera.targetTexture);

プロジェクター用のマテリアルに渡して、描画時にデプスを比較するようにします。

深度を比較して塗らないようにする

プロジェクター用のシェーダーを少し変更します。

Shader "Unlit/Projector"
{
    Properties
    {
        _InkTex ("InkTex", 2D) = "white" {}
        _DepthTex ("DepthTex", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        ZWrite Off
        ColorMask RGB
        // アルファブレンド
        Blend SrcAlpha OneMinusSrcAlpha
        Offset -1, -1

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _InkTex;
            sampler2D _DepthTex;
            float4x4 _ProjectorVP;
            float4 projector_LightmapST;

            v2f vert (appdata v)
            {
                v2f o;
                
                // ライトマップのuvに変換
                v.uv2 = v.uv2.xy * projector_LightmapST.xy + projector_LightmapST.zw;
                #if UNITY_UV_STARTS_AT_TOP
                    v.uv2.y = 1.0 - v.uv2.y;
                #endif

                // 頂点をuv座標にする
                o.vertex = float4(v.uv2 * 2.0 - 1.0, 0.0, 1.0);
                o.uv = ComputeScreenPos(mul(mul(_ProjectorVP, unity_ObjectToWorld), v.vertex));
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2Dproj(_InkTex, UNITY_PROJ_COORD(i.uv));

                float4 uv = i.uv / i.uv.w;

                #if UNITY_UV_STARTS_AT_TOP
                    uv.y = 1.0 - uv.y;
                #endif

                fixed depth = tex2D(_DepthTex, uv).r;
                #if defined(UNITY_REVERSED_Z)
                    clip(uv.z - depth + .0001);
                #else
                    clip(depth - uv.z + .0001);
                #endif

                fixed3 isOut = step((uv - 0.5) * sign(uv), 0.5);
                float alpha = isOut.x * isOut.y * isOut.z;
                
                return col * alpha;
            }
            ENDCG
        }
    }
}

note

Properties
{
    _InkTex ("InkTex", 2D) = "white" {}
    _DepthTex ("DepthTex", 2D) = "white" {}
}

...

sampler2D _DepthTex;

デプス画像を追加します。

float4 uv = i.uv / i.uv.w;

#if UNITY_UV_STARTS_AT_TOP
    uv.y = 1.0 - uv.y;
#endif

fixed depth = tex2D(_DepthTex, uv).r;

tex2Dで使えるuv(0〜1)にするために、ComputeScreenPosで作ったuvをwで割っています。この時uv.zがデプスの値になっています。

fixed depth = tex2D(_DepthTex, uv).r;
#if defined(UNITY_REVERSED_Z)
    clip(uv.z - depth + .0001);
#else
    clip(depth - uv.z + .0001);
#endif

プロジェクターから見たデプスと現在描画しようとしているピクセルのデプスを比べて、奥にある場合はclipして描画しないようにしています。また、Zファイティングを抑制するために+ .0001しています。(同じオブジェクトを描画するので確実にZファイティングするため。)

塗られない様子

これだけで完成です。

memo

UNITY_UV_STARTS_AT_TOPUNITY_REVERSED_Zを使ってプラットフォーム間の違いを吸収していますが、完全に正しいかは自信がありません。カメラを介して描画する時はunityがいい感じに吸収してくれますが、今回のように自分で描画する場合いろいろ自分で対応する必要がありました。

o.uv = ComputeScreenPos(mul(mul(_ProjectorVP, unity_ObjectToWorld), v.vertex));

ComputeScreenPosを使っていますが、ComputeScreenPosは内部で_ProjectionParamsを使っているので、これはうまく動かないかもしれません。

おわり

ステンシルっぽい表現も。

スプレーアートみたい

他に参考にした記事・サイト