【Unity】RenderTextureを使ってオブジェクトにアウトラインを付ける


こんにちはっ🌟八ツ橋まろんです

今回はUnityのアウトラインシェーダー&マテリアルの話です。

Unityで3Dモデルにアウトラインを付ける方法はいくつかありますが、以下の条件をお求めの人におすすめです。

・アウトラインを付けたい対象のオブジェクトのシェーダーは何でもOK
 (アウトライン機能のないスタンダードシェーダーとかでもOK)
・アウトラインを極太にしてもヘンな描画にならない(ただし描画コストが上がる)

アウトライン描画の概要

本記事でのアウトラインの描画の流れを以下の画像に示します。

こんな感じで、破綻しづらく太いアウトラインができました。
RenderTextureに対してシェーダーで処理をするので、対象の3Dモデルはどんなシェーダーでも良いというのがgoodです✨
(よくあるアウトライン描画の方法では、Stencilを仕込んだり、メッシュを拡張するためににシェーダーをいじる必要があるのですよね)

また、Render Textureを後ろに重ねるだけなので、二重や三重にすることもできるので、上の画像の一番右のように3Dモデル→白枠→細い灰色枠→影と言う風に3枚のRender Textureでより豪華な表現ができます。

シェーダーコード

以下、シェーダーコードです。

MaronOutlineShader.cs
Shader "Custom/MaronOutlineShader"
{
    Properties
    {
        [NoScaleOffset]_MainTex ("Render Texture", 2D) = "white" {}
        _SubTex ("Pattern Texture", 2D) = "white" {}
        _Color ("Main Color", Color) = (1,1,1,1)
        _Outline ("Outline thickness", Range (0,1)) = 0.1
        _DrawNum ("Drawing times", Range (2,32)) = 2
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
        LOD 100
        ZWrite off
        Blend srcAlpha OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #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 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            sampler2D _SubTex;
            float4 _SubTex_ST;
            float4 _SubTex_TexelSize;
            float4 _Color;
            float _Outline;
            int _DrawNum;

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

            float2 PolarToCartesian(float r, float theta)
            {
                float x = r * cos(theta);
                float y = r * sin(theta);
                return float2(x, y);
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);

                float2 tiling_offset_uv = float2(i.uv.xy *  _SubTex_ST *float2(1, _MainTex_TexelSize.x/_MainTex_TexelSize.y) +  _SubTex_ST.zw);
                fixed4 subcol = tex2D(_SubTex, tiling_offset_uv);

                float2 outline = _Outline*0.1;

                float PI = 3.14159265358979323;
                int n = _DrawNum;
                float theta;
                for(int j = 0; j < 2 * n; j++)
                {
                    theta = PI * j / n;
                    col += tex2D(_MainTex, i.uv + PolarToCartesian(outline, theta)*float2(_MainTex_TexelSize.x/_MainTex_TexelSize.y, 1));               
                }
                //境界をなめらかに
                col = smoothstep(0.5, 0.51, col);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);




                //一定以下のalphaはゼロにする
                _Color.a *= step(1-col.a, 0.01);

                return subcol*_Color;
            }
            ENDCG
        }
    }
}

このシェーダーのキモは、以下のfor文の部分と、PolarToCartesianと定義した関数部分です。

for文
for(int j = 0; j < 2 * n; j++)
{
    theta = PI * j / n;
    col += tex2D(_MainTex, i.uv + PolarToCartesian(outline, theta)*float2(_MainTex_TexelSize.x/_MainTex_TexelSize.y, 1));               
}
PolarToCartesian関数
float2 PolarToCartesian(float r, float theta)
{
    float x = r * cos(theta);
    float y = r * sin(theta);
    return float2(x, y);
}

for文は2n回だけループしますが、ループのたびにthetaがπ/nだけ変化し、PolarToCartesian関数に代入されます。
そして、PolarToCartesian関数は極座標→直交座標の変換をする関数です。( (r, θ)表記を(x, y)表記に直しています)
rはアウトラインの太さ(ずれの大きさ)です。
つまり、例えばn = 2のとき、π/2 = 90度なので、90度ずつrだけずれた描画が2n = 4回なされて、RenderTextureと同じものが上下左右に描画されます。n = 2なのでX軸Y軸の2軸分ずらしたものを描画しているわけです(下画像:左)

(下方向の影は見づらいけど、脚が下方向に伸びていますね)
n = 2では、ごく短い距離にしないとアウトラインとして成立しないですが、これを、n = 4で影が8つ、n = 8で影が16という風に増やしていくと、太くてもきれいなアウトラインになります。

シェーダの核の部分はこんな感じです。
あとは、シェーダーをTransparentにしたり、模様を付けたいとき用にサブのテクスチャを設定できるようにしてtiling, offsetに対応したり、境界部分をsmoothで多少滑らかにしたりという感じです💡

以上、アウトラインシェーダーの話でしたっ🌟
もし良ければイイネ押していってくださいね💕

八ツ橋まろん

Twitter
https://twitter.com/Maron_Vtuber
Booth
https://maronvtuber.booth.pm/