【Unity】ボリュームカーソル【Shader】

はじめに

 オブジェクトを3D空間に配置する際のカーソルが必要だったので考えてみました。
 検索するにもなんていうのかわからないので、ボリューム感があるカーソルということで「ボリュームカーソル」とします。

結果

こんなんできました。

目標

見た目的に 近いのはこちらの Shield shader ですが、

やりたいのは
・キューブ状のカーソル
・サイズは自由に変更可能
・エッジはくっきり
です。

ちなみにこちらのシールドは球限定のようなので、そのまま使えません。

やっていること

Shield shader では

  • 交差部分を震度バッファを見ながら 白くする
  • rim shader で球の輪郭を浮き上がらせる
  • テクスチャーを時間軸でアニメーションさせる

のようで、rimは球限定の仕様だったため別の手段を考察します。

  • 交差のエッジ表現はプロジェクターで描画する。
  • rim の代わりに アウトラインを利用する
  • アニメーションはそのまま利用する

アウトラインでエッジを書く

前回の アウトラインを再考察してみた で輪郭を描くことにします。rim の代わりにエッジを表現します。
 前回と同じなので詳細は省きます。

交差部分のエッジを書く

深度バッファを調べてその差でエッジを書くを試してみたのですが、あまりにも汚かったため今回は Projector を使って 接触部分に矩形を描くことで代用することにしました。

プロジェクターの作成

// プロジェクター作成
var projectorObject = new GameObject( "ProjectorObject");
projectorObject.transform.position = new Vector3( 0, 100, 0);
projectorObject.transform.rotation = Quaternion.AngleAxis( 90, Vector3.right);
//projectorObject.hideFlags = HideFlags.HideInHierarchy | HideFlags.DontSaveInEditor;
projector = projectorObject.AddComponent<Projector>();
projector.nearClipPlane = 0.1f;
projector.farClipPlane = 200.0f;
projector.orthographic = true;
projector.orthographicSize = 64;
projector.material = new Material( Shader.Find( "Custom/ProjectorCursor"));
projector.material.SetFloat( "_Thickness", outlineWidth);
projector.material.SetFloat( "_Width", transform.lossyScale.x);
projector.material.SetFloat( "_Depth", transform.lossyScale.z);
projector.material.SetVector( "_Origin", new Vector4( transform.position.x, transform.position.y, transform.position.z, 0));
projector.material.SetColor( "_Color", outlineColor);

アウトライン生成後にでも 新規オブジェクトを生成し Projectorコンポーネント を追加します。もしHierarchy に余計なオブジェクトが出るのが嫌なら コメントアウトした部分 hideFlags を有効にしてください。エディッタに表示されなくなります。

少し高めのポジションに 真下を向くよう配置し 平行投影設定にし サイズは全ワールドが収まるサイズに設定しておきます。今回は 64 で 128×128 になります。

ギズモを表示するとこんな感じ

その後 専用のマテリアルを生成し設定します。

プロジェクター用のシェーダーを書く

長いですが全部の掲載します。

Shader "Custom/ProjectorCursor"
{
	SubShader
	{
		Pass
		{
			Tags
			{
				"RenderType" = "Opaque"
				"Queue" = "Transparent"
			}

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

			struct appdata
			{
				float4 vertex : POSITION;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 uv : TEXCOORD0;
				float4 outside_aabbox : TEXCOORD1;
				float4 inside_aabbox : TEXCOORD2;
				float orthographicSize : TEXCOORD3;
				float4 position : TEXCOORD4;
			};

			float4x4 unity_Projector;
			float4x4 unity_ProjectorClip;
			float3 _Origin; // カーソルの原点
			fixed4 _Color; // 色
			float _Width; // 幅
			float _Depth; // 奥行き
			float _Thickness; // ラインの太さ

			v2f vert( appdata v)
			{
				v2f o = (v2f)0;
				o.position = mul( unity_ObjectToWorld, v.vertex);
				o.vertex =  UnityObjectToClipPos( v.vertex);
				o.uv = mul( unity_Projector, v.vertex);
				float halfWidth = 0.5*_Width;
				float halfDepth = 0.5*_Depth;
				// 外側のサイズを求める
				o.outside_aabbox = float4(
					_Origin.x - halfWidth - 0.5*_Thickness,
					_Origin.z - halfDepth - 0.5*_Thickness,
					_Origin.x + halfWidth + 0.5*_Thickness,
					_Origin.z + halfDepth + 0.5*_Thickness
				);
				// 内側のサイズを求める
				o.inside_aabbox = float4(
					_Origin.x - halfWidth,
					_Origin.z - halfDepth,
					_Origin.x + halfWidth,
					_Origin.z + halfDepth
				);
				// プロジェクションのサイズを求める
				o.orthographicSize = 1.0/(2.0*abs( unity_Projector._11))*unity_ObjectToWorld._11;
				return o;
			}

			fixed4 frag( v2f i) : SV_Target
			{
				if( i.position.y < _Origin.y)
				{
					discard; // 中止
				}
				float2 pos = float2( (i.uv.x*2-1) * i.orthographicSize, (i.uv.y*2-1) * i.orthographicSize);
				if(
					i.inside_aabbox.x < pos.x &&
					i.inside_aabbox.y < pos.y &&
					i.inside_aabbox.z > pos.x &&
					i.inside_aabbox.w > pos.y
				)
				{
					// 内側の矩形より内側である
					discard; // 中止
				}
				if(
					i.outside_aabbox.x > pos.x ||
					i.outside_aabbox.y > pos.y ||
					i.outside_aabbox.z < pos.x ||
					i.outside_aabbox.w < pos.y
				){
					// 外側の矩形より外側である
					discard; // 中止
				}
				// 描画
				return _Color;
			}
			ENDCG
		}
	}
}

まず頂点シェーダーで矩形のサイズを計算

// 外側のサイズを求める
o.outside_aabbox = float4(
	_Origin.x - halfWidth - 0.5*_Thickness,
	_Origin.z - halfDepth - 0.5*_Thickness,
	_Origin.x + halfWidth + 0.5*_Thickness,
	_Origin.z + halfDepth + 0.5*_Thickness
);
// 内側のサイズを求める
o.inside_aabbox = float4(
	_Origin.x - halfWidth,
	_Origin.z - halfDepth,
	_Origin.x + halfWidth,
	_Origin.z + halfDepth
);

ここで投影する四角の 外側と内側の AABBox を計算します。

図にするとこんなかんじ

あと 特筆 するのは

// プロジェクションのサイズを求める
o.orthographicSize = 1.0/(2.0*abs( unity_Projector._11))*unity_ObjectToWorld._11;

でしょうか。試行錯誤して射影行列から表示範囲の幅を算出する事ができました。
射影行列 unity_Projector の _11 は width/2 の逆数 が入っているので 2倍して1から割ってやると プロジェクターの幅が出ます。その後、投影先になるオブジェクトのスケール分拡大してます。これを忘れると、拡大したオブジェクトに投影すると小さめに投影されてしまいます。

フラグメントシェーダーではひたすらカリング

まず上の図の黄色い部分より内側だと表示しない

if(
	i.inside_aabbox.x < pos.x &&
	i.inside_aabbox.y < pos.y &&
	i.inside_aabbox.z > pos.x &&
	i.inside_aabbox.w > pos.y
)
{
	// 内側の矩形より内側である
	discard; // 中止
}

次にオレンジより外側だと表示しない

if(
	i.outside_aabbox.x > pos.x ||
	i.outside_aabbox.y > pos.y ||
	i.outside_aabbox.z < pos.x ||
	i.outside_aabbox.w < pos.y
){
	// 外側の矩形より外側である
	discard; // 中止
}

それ以外なら矩形の中だから 色塗っていいよ

return _Color;

という流れです。殆どのピクセルが discard されるわけですが、もっと良い方法がありそうな気がしますが。とりあえずこれでOK
頭の

if( i.position.y < _Origin.y)
{
	discard; // 中止
}

はカーソルが中に浮いているときにも下に投影されてしまうので、それを防いでます。これはアプリの仕様次第なので適当に直してください。

カーソルにアニメーションさせる

これに関しては Shield shader を パクリ オマージュして時間軸に合わせてテクスチャをアニメーションさせてます。

// 参考
// https://medium.com/@aarhed/shield-shader-85cdaf903db7

Shader "Custom/Wave"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_Color ("Color", Color) = (1,1,1,1)
	}
	SubShader
	{
		Tags
		{
			"RenderType"="Transparent"
			"Queue"="Transparent"
			"IgnoreProjector" = "True"
		}
//		Blend One One
		Blend SrcAlpha OneMinusSrcAlpha
//		Blend SrcAlpha SrcColor
		ZWrite Off
		Cull Off
		Offset -1, -1

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

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
			};

			sampler2D _MainTex;
			float4 _Color;

			float triWave( float t, float offset, float yOffset)
			{
				return saturate( abs( frac( offset - t) * 2 - 1) + yOffset);
			}

			v2f vert( appdata v, out float4 vertex : SV_POSITION)
			{
				v2f o;
				vertex = UnityObjectToClipPos( v.vertex);
				o.uv = v.uv;
				return o;
			}

			fixed4 frag( v2f i, UNITY_VPOS_TYPE vpos:VPOS) : SV_Target
			{
				fixed4 col = tex2D( _MainTex, i.uv);
				float signal = triWave( _Time.x*5, vpos.y/_ScreenParams.y*3, -0.1)*0.7;
				col.rgb *= _Color;
				col.a = signal;
				return col;
			}
			ENDCG
		}
	}
}

Shield shader はオブジェクトの高さをもとにオフセットを出していますが、こちらは高さがコロコロ変わるので、スクリーン座標でオフセット値を決めてます。
アルファの Blend 具合は好みで設定してください。

Shield shader でも解説がありますが、 triWave の出力が 一定の間隔で ピークが尖った値を返す 線形関数 です。 abs(sin()) などでも近い値が出ますが、間隔とオフセットを指定できるので、便利な関数だと思います。

Shield shader より転載

あとはこのシェーダーをカーソルとなるキューブに設定すればアニメーションします。

パスル波形通りカラーが出ている図

完成

rim は アウトライン で代用
交差の白い部分は プロジェクター で代用
カーソルアニメーションは 多少変更し対応

見た目はこんな具合です。

おわりに

 想定通りに動きました。これでオブジェクトの配置や、選択に使えるものができた感じです。
 つくってみて、プロジェクターの応用でボリュームライトの塵の表現とかできそうな感じです。今度試してみます。
 あと、正式名わかる方いらっしゃいましたら、教えてもらえると助かります。

One Comment

Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です