The jonki

呼ばれて飛び出てじょじょじょじょーんき

【Unity】 Unityのシェーダで2Dのリング(円)を描く

UnityでdrawCircle的なソリッドな円を描くにはどうしたらいいんだろう、と調べたところあんまり楽ちんな方法がなかったのでシェーダー描いてみました。リングのテクスチャを貼ってそれをスケールする方法は手軽だけど、枠線がスケールによって変わっちゃうしね。

こんなの作りました。



ただこれだと結構ギザギザしてしまうので縁をアンチエイリアシング的な施しをします。これは記事の最後で説明します。

Unityでシェーダーを初めて自分で描く人のために

ちょっと使ったことがある、という想定のもと進みますので、カスタムシェーダーを作ったことがない人はこちら様のサイトで勉強すると良いかもしれません。

シェーダ: 頂点およびフラグメント プログラム / Shaders: Vertex and Fragment Programs

Unityのシェーダーに関してすべてではないですが、日本語翻訳があります。サーフェイスシェーダーという光源周りのコードが楽ちんにかけるシェーダも見逃せません。ただし今回は光の設定を使わないので使わないほうが良いですが、文法周りはかなり似ています。

その1 UnityにおけるShaderとは?

主にサーフェイスシェーダの解説が載っていますが、Unityのシェーダーの使い方自身にも説明があるので助かります。
初めての人はここを見ておくと手っ取り早くシェーダーの使い方を体験できます。

シェーダーで穴あきリングを作る!

方針としては、手軽に書けるものをやりたかったので、下のようになりました。
貼り付けたオブジェクト(今回はPlaneでやりました)の中心に対して、各ピクセルで距離を計算します。
そしてそのピクセルの位置が_InnerRadiusと_OuterRadiusの間にある場合は、色を赤に、それ以外はclip関数で描画を省いています。Propertiesとしてこれらのパラメタは公開しているので、UnityのInspectorから調整できます。日記冒頭の画像の右側にちゃんと見えていますね。テクスチャが未割り当てですが、領域だけほしいので特に問題ありません。

Shader "Custom/RingShader" {
	Properties {
	    // properties for water shader
	    _MainTex ("Texture", 2D) = "white" { }
	    _InnerRadius ("Inner Radius", Range (0.0, 1.0)) = 0.3 // sliders
	    _OuterRadius ("Outer Radius", Range (0.0, 1.0)) = 0.2 // sliders
	} 
	
	SubShader {
		Pass {
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			uniform sampler2D _MainTex;
			uniform float _InnerRadius;
			uniform float _OuterRadius;
			
			struct v2f {
			    float4  pos : SV_POSITION;
			    float2  uv : TEXCOORD0;
			};
			
			float4 _MainTex_ST;
			
			v2f vert (appdata_base v)
			{
			    v2f o;
			    o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
			    o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
			    return o;
			}
			
			half4 frag (v2f i) : COLOR
			{
		    	    half4 texcol = float4(0.0, 0.0, 0.0, 1.0);
		    	    float dist = distance(i.uv, float2(0.5,0.5));
		    	    if(dist < _InnerRadius) {
		    		clip(-1.0);
		    	    } else if(dist < _OuterRadius) {
		    		texcol.r = 1.0;
		    	    } else {
		    		clip(-1.0);
		    	    }
		    	
		    	    return texcol;
			}

			ENDCG
		}
	}
}

スクリプトからシェーダーで使うパラメタを修正する

Inspectorから値をいじれるようになったものの、やはりもっと動的にスクリプトから色々したいところです。Unityにはちゃんとその辺りよく考えられていて、Propertiesをスクリプトから直接いじれるようになってます。上のシェーダーがセットされているマテリアルが付いているGmaeObjectに下記のスクリプトを追加してみてください。マウスの位置によって円の形が変わります。

C#とシェーダーの間で値をGet/Setするのが簡単にできます。他のパラメタ(float4やColor等)も同様の方法でいじれます。めっちゃ便利ですね。

using UnityEngine;
using System.Collections;

public class RingController : MonoBehaviour {

	// Use this for initialization
	void Start () {
	
	}
	
	// Update is called once per frame
	void Update () {
		Debug.Log("InRad =" + renderer.material.GetFloat("_InnerRadius"));
		Debug.Log("OutRad=" + renderer.material.GetFloat("_OuterRadius"));
		
		renderer.material.SetFloat("_InnerRadius", Input.mousePosition.x / Screen.width);
		renderer.material.SetFloat("_OuterRadius", Input.mousePosition.y / Screen.height);
	}
}

リングのギザギザを滑らかにする

ソリッドな円ができましたが、円の縁がギザギザして格好わるいので滑らかにします。色々やり方はあると思いますが、今回は内円と外円から_BlurThickness分だけ描画します。縁から離れるほど透過度を落としていけば、縁がぼやけるので綺麗になります。_BlurThicknessもPropertiesに設定してあるのでInspectorやスクリプトから好みに合わせて調整すると良いと思います。これをやると冒頭の2つ目の画像みたいになります。

Shader "Custom/RingShader" {
	Properties {
	    // properties for water shader
	    _MainTex ("Texture", 2D) = "white" { }
	    _InnerRadius ("Inner Radius", Range (0.0, 1.0)) = 0.3 // sliders
	    _OuterRadius ("Outer Radius", Range (0.0, 1.0)) = 0.2 // sliders
	    _BlurThickness ("Blur Thickness", Range (0.0, 1.0)) = 0.2 // sliders
	} 
	
	SubShader {
		// for transparent
		ZWrite Off
		Blend SrcAlpha OneMinusSrcAlpha 
		
		Pass {
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			uniform sampler2D _MainTex;
			uniform float _InnerRadius;
			uniform float _OuterRadius;
			uniform float _BlurThickness;
			
			struct v2f {
			    float4  pos : SV_POSITION;
			    float2  uv : TEXCOORD0;
			};
			
			float4 _MainTex_ST;
			
			v2f vert (appdata_base v)
			{
			    v2f o;
			    o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
			    o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
			    return o;
			}
			
			half4 frag (v2f i) : COLOR
			{
		    	    half4 texcol = float4(1.0, 0.0, 0.0, 1.0);
		    	    float dist = distance(i.uv, float2(0.5,0.5));
		    	    if(dist < _InnerRadius - _BlurThickness) {
		    		clip(-1.0);
		    	    } else if(dist < _InnerRadius) {
		    		texcol.w = (dist - (_InnerRadius - _BlurThickness)) / _BlurThickness;
		    	    } else if(dist < _OuterRadius) {
		    		texcol.r = 1.0;
		    	    } else if(dist < _OuterRadius + _BlurThickness) {
		    		texcol.w = 1.0 - (dist - _OuterRadius) / _BlurThickness;
		    	    } else {
		    		clip(-1.0);
		    	    }
		    	
		    	    return texcol;
			}

			ENDCG
		}
	}
}