SPARKCREATIVE Tech Blog

https://www.spark-creative.jp/

GLSLSandBoxのコードをUnityに移植する ~応用編~

こんにちは、クライエントエンジニアの中島龍清(ナカシマリュウセイ)です。

今回は前回は整理したシェーダーの処理を応用した新たなシェーダーの作成をおこなっていこうと思います。
前回、前々回の内容は以下から確認できますので、興味があったら読んでくださると嬉しいです。
前回:GLSLSandBoxのコードをUnityに移植する ~解説と整理編~ - SPARKCREATIVE Tech Blog
前々回:【Unity】GLSLSandboxのコードをUnityに移植する - SPARKCREATIVE Tech Blog

はじめに

今回作成するシェーダーは最終的に以下のような描画結果となります。

最終的な描画結果

前回作成したものとは大きく見た目が異なりますが、処理のほとんどはもともとあった処理の応用になっています。
まずは、前回のシェーダーを改めて確認します。
全体の内容は以下になります。

// https://glslsandbox.com/e#75651.0

Shader "Unlit/GLSL2HLSL"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
	_Mouse ("MousePos", Vector) = (0,0,0,0)
	_Resolution ("Resolution", Vector) = (1,1,1,1)

	//_Angle("Angle", Range(0,1)) = 0		//確認用
	//_Width("Width", Range(0,1)) = 0.02		//確認用
	//_Radius("Radius", Range(0,1)) = 0.5		//確認用
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            #define PI 3.14159265359

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float2 _Mouse;
            float2 _Resolution;

	    //float _Angle;		//確認用
	    //float _Radius;	        //確認用
	    //float _Width;		//確認用

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

	    float2x2 rotate2d(float angle) 
	    {
		return float2x2(cos(angle), sin(angle) , -sin(angle), cos(angle));
	    }

            fixed4 frag (v2f i) : SV_Target
            {
		// UVを0 ~ 1 から -1 ~ 1に変換
		float2 p = (i.uv * 2.0f - float2(1.0f,1.0f));
		// 回転角度(ラジアン)
		float angle = _Time.y / 2.0f * PI;
		// 回転行列を生成する関数に回転角度を渡す
		p = mul(rotate2d(angle), p);
		// 太さ
		float width = 0.02f;
		// 半径 時間によって0~1に変化
		float radius = abs(sin(_Time.y));

		//angle = _Angle * 2.0f * PI;	//確認用
		//width = _Width;		//確認用
		//radius = _Radius;		//確認用

		// 時間で円の大きさを変化 length(p)は中心からの距離
		float t = width / abs(radius - length(p));
		// 色の決定
		float3 color;
		color.r = t * p.x;
		color.g = t * p.y;
		color.b = t;
		return fixed4(color, 1.0);
            }
            ENDCG
        }
    }
}

作成手順

2Dから3Dへ

はじめに、2Dで描画される円を3Dに変換させます。
前回のシェーダーでUVの値を使用して円を描画しているところを
3DモデルのXとZの値に置き換えたいので3Dモデルの頂点座標を取得します。
頂点座標はappdata構造体でセマンティックによって取得しており、
vert関数内でクリップ座標に変換したものを戻り値として渡しています。
しかし、取得したいのは座標変換されていない頂点座標(ローカル座標)なのでv2f構造体に変数を新規作成し、
その変数にそのままの頂点座標を保持させます。

struct v2f
{
        float2 uv : TEXCOORD0;
        float4 vertex : SV_POSITION;
	float3 vertPos : TEXCOORD1;	//追加
};
v2f vert (appdata v)
{
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.uv = TRANSFORM_TEX(v.uv, _MainTex); 
	o.vertPos = v.vertex;	//追加
        return o;
}

これにより3DモデルのX,Y,Zの値が取得できるようになったので、UVに依存していた箇所をXとZに置き換えます。
【元の処理】

// UVを0 ~ 1 から -1 ~ 1に変換
float2 p = (i.uv * 2.0f - float2(1.0f,1.0f));

【置き換え後】

//頂点座標のXZのみ抜き出す
float2 p = i.vertPos.xz;

現段階での描画結果を確認するために適当な3Dオブジェクト(今回はCube)にマテリアルをアタッチしてみます。
描画結果は以下のようになっていると思います。

UV依存の箇所をXZ依存に置き換えたもの

現状だとオブジェクトの水平面に円が描画されていますが、
最終的に円の描画ではなくオブジェクトの水平方向にラインが引かれるような見た目にしたいので以下のように書き換えます。
【元の処理】

// 時間で円の大きさを変化 length(p)は中心からの距離
float t = width / abs(radius - length(p));

【置き換え後】

// 高さに依存
float t = width / abs(y);

水平ということは描画したい箇所の高さが同じであるということです。
なのでYの値に置き換えると同じ高さの箇所に色が付き、ラインが引かれているように見えます。
この変更によって以下のような描画結果になっていると思います。

円からラインになったもの

※座標系については以下のサイトが丁寧に説明してくださっているのでご参照ください。
xr-hub.com

テクスチャの適用

次に、円以外の黒色になっている箇所にテクスチャを適用させます。
黒色ということはカラーの値は0か限りなく0に近い状態のため、
テクスチャの色を加算することで円以外の箇所がテクスチャの色がほぼそのまま描画されるようになります。
カラー値にテクスチャの色を加算する処理を追加します。
【元の処理】

// 色の決定
float3 color;
color.r = t * p.x;
color.g = t * p.y;
color.b = t;
return fixed4(color, 1.0);

【置き換え後】

// 色の決定
float3 color;
color.r = t * p.x;
color.g = t * p.y;
color.b = t;
// テクスチャの色
float3 textureColor = tex2D(_MainTex, i.uv).rgb;
return fixed4(color + textureColor, 1.0);

この変更によりテクスチャが適用され、以下のような描画結果になっていると思います。

テクスチャを適用したもの
使用テクスチャ
透過処理の追加

最後に透過処理を加えていきます。
透過処理を適用させるにはRenderTypeとQueueをTransparentにする必要があるので変更します。
そしてBlending TypeはBlend SrcAlpha OneMinusSrcAlphaを指定します。

Tags { "RenderType"="Transparent" "Queue" = "Transparent"} //Transparentにして透過適用
Blend SrcAlpha OneMinusSrcAlpha // BlendingTypeの指定

これで透過処理が適用可能になりました。
※BlendingTypeについてはUnity公式ドキュメントをご参照ください。
docs.unity3d.com

透過が適用可能になったのでラインより上の部分だけ透過させる仕組みを作成し、アルファ値として出力させます。

// 透過値
float alpha = saturate(step(y, 0) + color.r * color.g);
return fixed4(color + textureColor, alpha);

ラインの位置はYの値で取れるのでそれを利用してA値が0になるように調節しています。
後は一部のパラメータを調整することにより最初に載せた画像と同じ状態になります。

最終的な描画結果

改めて全体のコードを載せると以下のようになっております。

// https://glslsandbox.com/e#75651.0

Shader "Unlit/GLSL2HLSL"
{
    Properties
    {
		_MainTex("Texture", 2D) = "white" {}

		//_Angle("Angle", Range(0,1)) = 0			//確認用
		//_Width("Width", Range(0,1)) = 0.02		//確認用
		//_EffHeight("EffHeight",Float) = 0			//確認用
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent"} //Transparentにして透過適用
		Blend SrcAlpha OneMinusSrcAlpha // BlendingTypeの指定
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

			#define PI 3.14159265359

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
		float3 vertPos : TEXCOORD1;	
            };

			sampler2D _MainTex;
			float4 _MainTex_ST;

			//float _Angle;		//確認用
			//float _Width;		//確認用
			//float _EffHeight;	//確認用

            v2f vert (appdata v)
            {
                v2f o;
		//頂点座標をクリップ座標に変換
                o.vertex = UnityObjectToClipPos(v.vertex);
		// UV
                o.uv = TRANSFORM_TEX(v.uv, _MainTex); 
		//そのままの頂点座標を保持
		o.vertPos = v.vertex;
                return o;
            }

			// 回転行列計算
			float2x2 rotate2d(float angle) 
			{
				return float2x2(cos(angle), -sin(angle), sin(angle), cos(angle));
			}

			fixed4 frag(v2f i) : SV_Target
			{
				//頂点座標のXZのみ抜き出す
				float2 p = i.vertPos.xz;
				//yのみ抜き出す
				float y = i.vertPos.y;
				//回転角度(ラジアン)
				float angle = _Time.y / 2.0f * PI;
				// 回転行列を生成する関数に回転角度を渡す
				p = mul(rotate2d(angle), p);
				// 太さ
				float width = 0.1f;	//見た目調整として0.1に変更

				//angle = _Angle * 2.0f * PI;	//確認用
				//width = _Width;				//確認用
				//y = y + _EffHeight;			//確認用
				y -= 0.2f;	//見た目調整用

				// 高さに依存
				float t = width / abs(y);
				// 色の決定
				float3 color;
				color.r = t * p.x;
				color.g = t * p.y;
				color.b = t;
				// テクスチャの色
				float3 textureColor = tex2D(_MainTex, i.uv).rgb;
				// 透過値
				float alpha = saturate(step(y, 0) + color.r * color.g);
				return fixed4(color + textureColor, alpha);
			}

            ENDCG
        }
    }
}

「確認用」とコメントされている箇所をコメントアウト解除するとプロパティとしてそれぞれの値を操作可能になります。

まとめ

はじめに述べたように、ほとんどが以前のシェーダーを応用した処理だったことが確認できたのではないでしょうか?
各処理がそれぞれ描画結果に対してどんな影響を与えているかをしっかり把握することで、
今回のように一つのシェーダーから新しいシェーダーを生み出すことが可能になります。
サンプルシェーダーを引っ張ってきて自分好みのシェーダーに改造していくのは結構楽しいので是非チャレンジしてみてください。