かつて代官山らへんで働いてたengineerのUnityブログ

サーバサイドやってきたエンジニアがUnityとか触って遊ぶだけのブログ

【Unity】ワープポータル転送演出の作り方【AdventCalendar2022】

Unity Advent Calendar 2022 8日目を書きます
皆さんワープポータルは好きですか?
僕は好きです

どんなワープポータル

でもワープポータルって言っても色々ありますよね
あんなのやこんなの、表現も色々なものがあると思いますが...

今回作ろうとしたのはこんなポータル演出

これを見た皆さんはこんな作り方を一瞬で想像したことでしょう
①ポータルのエフェクトを足元に出す
②プレイヤーを地面方向にアニメーションさせる
③地面に埋まったらプレイヤー座標を移動先に移す
④移動先にポータルエフェクトを足元に出す
⑤プレイヤーを上方向にアニメーションさせる

実際その通り実装しています
が!この実装には大きな問題があります


おわかりいただけただろうか、橋をゴリラがすり抜けていくのを...🍌

御覧の通り、上記の実装は橋などがあるゲームステージを想定出来ていません
僕は実装してから気付きました

ではどうすればよいのか
プレイヤーの上下移動と連動して、ポータルに沈み込んだ体の部分を消してしまえばよいですね
今回はその方法についていくつか検討してみようと思います

方法① 重なった部分が透明になるオブジェクトでマスクする

「プレイヤー」「マスク用透明オブジェクト」「その他(地面など)」これらの描画優先度を変更することでプレイヤーの体の一部を透明にする方法です

Tags {"Queue" = "Geometry"}

プレイヤーのシェーダーにこのタグを追加してください

Tags {"Queue" = "Geometry-2"}

地面のシェーダーにこのタグを追加してください

Shader "Custom/Cutout" {
    SubShader{
        Tags {"Queue" = "Geometry-1"}

        Pass{
            ColorMask 0
        }
    }
}

このシェーダーを適用した「マスク用透明オブジェクト」を用意してください

インスペクターでそれぞれのオブジェクトのマテリアルのRender Queueの数字の大きさが
「地面」→「マスク用透明オブジェクト」→「プレイヤー」
になっていることを確認してください

あとは「マスク用透明オブジェクト」を消したいオブジェクトと被せて配置すれば


完成です、お手軽ですね
少しネックなのは地面などのマスクしたくないオブジェクトのRender Queueの数値を場合によっては変更しなければいけないことでしょうか

参考にさせていただいたのはこちらの記事です
nn-hokuson.hatenablog.com

方法② 平面オブジェクトでクリッピングする

平面のオブジェクト(Plane)を用意し、表よりも上にあれば表示、裏よりも下にあれば描画を破棄する、という方法です

Shader "Custom/Player"
{
    Properties
    {
        _MainTex("Texture", 2D) = "white" {}

        [Toggle] _CullEnable("Cull Enable", Float) = 1
        [Toggle] _Positive("Cull Positive", Float) = 1
    }
        SubShader
    {
        Tags {
            "RenderType" = "Opaque"
        }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _CullEnable;
            float _Positive;
            uniform float4 _ClippingPlane;

            v2f vert(appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);

                if (_CullEnable == 1)
                {
                    o.viewVertex = mul(UNITY_MATRIX_MV, v.vertex);
                }

                return o;
            }

            fixed4 frag(v2f i) : SV_Target
            {
                if (_CullEnable == 1)
                {
                    float4 plane = _ClippingPlane;
                    if (_Positive == 0)
                    {
                        if (dot(plane.xyz, i.viewVertex.xyz) > plane.w)
                        {
                            discard;
                        }
                    }
                    else
                    {
                        if (dot(plane.xyz, i.viewVertex.xyz) < plane.w)
                        {
                            discard;
                        }
                    }
                }

                fixed4 col = tex2D(_MainTex, i.uv);

                return col;
            }
        ENDCG
        }
    }
}

このシェーダーをプレイヤーのマテリアルに適用します
既に使っているシェーダーがある場合は頑張って合体させてください

using UnityEngine;

public class ClippingPlane : MonoBehaviour
{
    [SerializeField]
    private Transform _clippingPlane;

    private Material _material;

    private void Awake()
    {
        var skinnedMeshRenderer = GetComponent<SkinnedMeshRenderer>();
        _material = skinnedMeshRenderer.material;
    }

    private void Update()
    {
        Matrix4x4 viewMatrix = Camera.main.worldToCameraMatrix;

        Vector3 viewUp = viewMatrix.MultiplyVector(_clippingPlane.up);
        Vector3 viewPos = viewMatrix.MultiplyPoint(_clippingPlane.position);
        float distance = Vector3.Dot(viewUp, viewPos);
        Vector4 plane = new Vector4(viewUp.x, viewUp.y, viewUp.z, distance);

        _material.SetVector("_ClippingPlane", plane);
    }
}

このC#コードをプレイヤーオブジェクトにアタッチします

平面オブジェクト(Plane)を作ってアタッチしたClippingPlaneの_clippingPlaneのパラメータに参照させます
Planeを表示したくない場合は非アクティブにしても大丈夫です
あとはPlaneをプレイヤーの体を遮るように配置すれば


完成です。シェーダーだけでなくプログラムも絡んでくるので①よりも少しややこしいですが、Planeとプレイヤーだけで完結できるのが魅力ですね

参考にさせていただいたのはこちらの記事です
edom18.hateblo.jp

まとめ

以下が修正後です

しっかりポータルに入っていっている風になっているかと思います🌴

他にもブーリアン演算とかメッシュを無理やり変形させるとか方法は様々あるとは思うのですが
僕は方法②を使うことにしました
ポータル自体が平面の表現で相性が良かったことと、平面オブジェクトとプレイヤーのみで完結することが出来るためです
方法①の方は複数カメラを使ったゲームと相性がよかったりと、それぞれの利点があるので見せ方やゲーム内の状態によって使い分けで行くのがいいような気がします

ゴリラ・オンライン

www.youtube.com

最近知ったんですが、ゴリラオンラインっていうめちゃくちゃ面白いアプリがあるらしいです
海外セレブもこぞって遊んでいるとか...
今ならなんとゴリラオンラインが無料で遊べます!
是非遊んでみて!それでは~~~🦍

ゴリラ・オンライン

ゴリラ・オンライン

  • kosuke sato
  • ゲーム
  • 無料
apps.apple.com