ボアブログ

へっぽこUnityエンジニアの色々

ステンシルバッファでマスク処理やってみた

f:id:weakboar:20171130024531g:plain

割りと有名なやつですがステンシルバッファでマスク処理をやってみました。
急に思い立った理由がとある開発環境のUI周りでスクロールビュー的なアレで辛い思いをしたので自力で実装したついでのメモです。

まず始めにマスクをかける方のシェーダーです。よくあるやつですがそもそも描画事態はいらないのでアルファ値を0にしてます。テクスチャもなし。

Shader "Unlit/StencilMaster"
{
    Properties
    {
    }

    SubShader
    {
        Tags {"Queue"="Transparent"}
        Pass
        {
            Stencil {
                Ref 2
                Comp always
                Pass replace
            }
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag
            #include "UnityCG.cginc"
            
            fixed4 frag (v2f_img i) : SV_Target
            {
                return fixed4(0.0, 0.0, 0.0, 0.0);
            }
            ENDCG
        }
    }
}

次にマスクをかけられる方です。これも良くあるやつ。

Shader "Unlit/Stencil"
{
    Properties
    {
        _MainTex("-",2D)="white"{}
    }

    SubShader
    {
        Tags {"Queue"="Transparent+1"}

        Pass
        {
            Stencil {
                Ref 2
                Comp equal
            }
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM
            sampler2D _MainTex;
            #pragma vertex vert_img
            #pragma fragment frag

            #include "UnityCG.cginc"
            
            fixed4 frag (v2f_img i) : SV_Target
            {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }

    }
}

そして最後に、今回はUIで使うのが目的なので幅と高さから頂点を計算してEditor上でmeshを動的に生成します。

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

[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshFilter))]
public class UIMask : MonoBehaviour
{
    [SerializeField] float width = 1.0f;    // 横
    [SerializeField] float height = 1.0f;   // 縦
    [SerializeField] Material material;     // マテリアル、ステンシルバッファに書き込むmaterialを割り当てる
    [SerializeField] List<Vector3> vertices = new List<Vector3>();
    private Mesh mesh = null;

    Vector3[] defaultVertices = { new Vector3(-0.5f, -0.5f, 0), new Vector3(-0.5f, 0.5f, 0), new Vector3( 0.5f, 0.5f, 0), new Vector3( 0.5f, -0.5f, 0) };
    int[] triangles = { 0, 1, 2, 0, 2, 3 };

    public void UpdateMesh()
    {
        MeshFilter meshFilter = gameObject.GetComponent<MeshFilter>();

        // mesh無かったら作る
        if( meshFilter.sharedMesh == null )
        {
            meshFilter.sharedMesh = new Mesh();
        }
        // 矩形分の頂点は作っておく
        for (int i = vertices.Count; i < 4; i++)
        {
            vertices.Add(Vector3.zero);
        }
        // 余分な分は削除
        if( vertices.Count - 4 > 0)
        {
            vertices.RemoveRange(4, vertices.Count - 4);
        }

        // widthとheightから頂点計算
        for(int i = 0; i < vertices.Count; i++)
        {
            var defaultVert = defaultVertices[i];
            vertices[i] = new Vector3(defaultVert.x * width, defaultVert.y * height, defaultVert.z);
        }

        // meshに頂点設定、法線再計算
        meshFilter.sharedMesh.vertices = this.vertices.ToArray();
        meshFilter.sharedMesh.triangles = this.triangles;
        meshFilter.sharedMesh.RecalculateNormals();

        // MeshFilterとRendererにmeshとmaterial割当
        MeshRenderer meshRenderer = gameObject.GetComponent<MeshRenderer>();
        meshRenderer.sharedMaterial = material;
    }

    void OnValidate()
    {
        // mesh再計算
        UpdateMesh();
    }

    void OnDrawGizmosSelected()
    {
        var localPosition = this.transform.localPosition;
        for(int i=0; i < vertices.Count; i++)
        {
            var next = (i + 1) % vertices.Count;
            var start = vertices[i] + localPosition;
            var end = vertices[next] + localPosition;
            Gizmos.DrawLine(start, end);
        }
    }
}

割りと雑な作りですが値の変更時にmeshを再計算したり、meshに合わせてSceneView上に線を描画する処理も一応書いておきました。
Editor上で矩形を調整するようなのもやってみたかったけど一旦こんな感じです。