e.blog

主にUnity/UE周りのことについてまとめていきます

Unityでクリッピング平面を実装する

概要

今作っているARアプリでクリッピング平面を作る必要が出てきたのでそのメモです。

動作サンプル↓ f:id:edo_m18:20180508201124g:plain

動画サンプルで動かしたものはGitHubで公開しています。

ちなみに、クリッピング平面自体の説明は以前Qiitaに書いたのでそちらをご覧ください。
今回書くのはARアプリで使用したUnityでのクリッピング平面の実装メモです。

qiita.com

実装

今回、Unityで実装するに当たってクリッピング平面の位置や方向などはC#側で計算し、それをシェーダに渡す形で実装しました。

まずはC#コードを。

C#コード

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

public class ClippingPlanes : MonoBehaviour
{
    [SerializeField]
    private Transform[] _clippingPlanes;

    private Material _material;

    private void Awake()
    {
        _material = GetComponent<MeshRenderer>().material;
    }

    private void Update()
    {
        Vector4[] planes = new Vector4[_clippingPlanes.Length];
        Matrix4x4 viewMatrix = Camera.main.worldToCameraMatrix;

        for (int i = 0; i < planes.Length; i++)
        {
            Vector3 viewUp = viewMatrix.MultiplyVector(_clippingPlanes[i].up);
            Vector3 viewPos = viewMatrix.MultiplyPoint(_clippingPlanes[i].position);
            float distance = Vector3.Dot(viewUp, viewPos);
            planes[i] = new Vector4(viewUp.x, viewUp.y, viewUp.z, distance);
        }

        _material.SetVectorArray("_ClippingPlanes", planes);
    }
}

上記は、平面の方向と距離を算出するコードです。
大事なポイントは3つ。

  1. worldToCameraMatrixがビューマトリクス
  2. worldToCameraMatrix.MultiplyVectorで平面方向をビュー座標に変換
  3. worldToCameraMatrix.MultiplyPointで平面位置を変換、方向ベクトルに射影して距離を得る

です。

そして以下のコードで「平面の方向と距離」をVector4としてシェーダに渡します。

planes[i] = new Vector4(viewUp.x, viewUp.y, viewUp.z, distance);

// ... 中略

_material.SetVectorArray("_ClippingPlanes", planes);

ちなみに、複数プレーンでクリッピングができるようにしてみたのでプレーンの方向と距離は配列でシェーダに渡しています。

シェーダでは_ClippingPlanesを利用してクリッピングを実行します。
シェーダコードは以下です。

Shader "Unlit/ClippingPlane"
{
    Properties
    {
        [Toggle] _Positive("Cull Positive", Float) = 1
        _PlaneCount("Plane count", Range(0, 10)) = 0
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma multi_compile_fog
            
            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 viewVertex : TEXCOORD2;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Positive;
            uniform float _PlaneCount;
            uniform float4 _ClippingPlanes[10];
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                o.viewVertex = mul(UNITY_MATRIX_MV, v.vertex);
                return o;
            }
            
            fixed4 frag (v2f i) : SV_Target
            {
                int count = int(_PlaneCount);
                for (int idx = 0; idx < count; idx++)
                {
                    float4 plane = _ClippingPlanes[idx];
                    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);
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

最後に判定部分。
C#から送られてきた、ビュー座標空間での方向と距離を元にビュー座標空間上でのフラグメントの位置を内積を使って計算し、距離が平面より遠かったらクリッピングdiscard)する、という処理です。

また、今回のサンプルでは_PlaneCountで平面の数を指定できるようにしています。
というのもシェーダでは配列を可変にできないため予め領域を確保しておく必要があります。

なので、外部からいくつの平面が渡ってくるかを指定できるようにしている、というわけです。
今回はサンプルなのでひとまず最大10平面まで受け取れるようにしています。

実際問題、クリッピングを実行したい、という平面の数はそこまで多くならないと思います。
もし多数必要になったらテクスチャにして渡すなどある程度工夫が必要でしょう。

が、今回はあくまでクリッピング平面の実装メモなのでそのあたりは適当です。

余談

ちなみにこの判定は面白くて、どこかの質問サイトで見たのが、キャラクターが地面に接地しているか、の判定にまさにこの仕組みを利用していて、なぜこれで接地が判定できるのですか? という質問がありました。

方向と距離を渡して、あとは内積結果と比較するだけで判定が済むので圧倒的な省コストで判定できることになります。
ロジックとしては覚えておくと色々役に立ちそうです。

余談2

ちなみに、これとは別視点で実装した「異空間から現れてた」ような演出をするシェーダについて過去に書いたので興味があったら読んでみてください。

edom18.hateblo.jp