これはドリコム Advent Calendar 2016 22日目です。

はじめに

初めまして、入社一年目の中西と申します。
新卒クライアントエンジニアとして入社いたしました。
普段はCocos2d-xでスマートフォンアプリ開発を行っています。
Unityのシェーダーを扱うチームにも関わっており、今回は今まで書く機会が少なかったUnityのシェーダーを書いてみようということで記事を書くことにしました。

そこで、今回はUnityでバンプマッピングに挑戦したいと思います。

バンプマッピングについて

バンプマッピングとは、通常のテクスチャとノーマルマップ(法線情報を保持した画像)を組み合わせて、モデルの表面のデティールを表現する技術です。

通常のテクスチャ

通常のテクスチャ

法線マップ

法線マップ

モデルのライティングを行う際、基本的な考え方としてポリゴンの”面の向き”とライトの方向により計算を行う事が多いです。
“面の向き”はその面に垂直なベクトルとして表現でき、そのベクトルを”法線”と呼びます。
この法線を実際の面から取得するのではなく、法線マップから取得し計算を行うことで実際の面とは異なるライトの影響を与えることができます。

それを利用し、細かい凹凸を実際のポリゴンで表現するのではなく、法線マップで凹凸に見えるようにライトの影響を与えることで平面のポリゴンを凹凸があるように見せることができます。

※今回はノーマルマップを使用していますが、ハイトマップと呼ばれる高さ情報からバンプマップを実装する方法も存在します。
バンプマップの詳しい説明はUnityのマニュアルにも記載されていますのでご参照ください。
Unity-マニュアル:法線マップ

実装方法

まずは今回実装したシェーダー全文です。

Shader "Custom/bumpmap" {
	Properties {
		_MainTex("Texture",   2D) = "white" {}
		_NormalTex("NormalTex", 2D) = "white" {}
	}
	SubShader {
		Tags { "RenderType"="Opaque" }
		LOD 200

		Pass
		{
			CGPROGRAM
			// 頂点シェーダー使う時用
        	#pragma vertex vert
        	// フラグメントシェーダー使う時用
        	#pragma fragment frag

        	#include "UnityCG.cginc"

			uniform sampler2D _MainTex;
			uniform sampler2D _NormalTex;


			// 頂点シェーダーに渡すもの
			struct v_in{
				float4 vertex : POSITION; 	//頂点座標
				float4 normal : NORMAL;		//法線
				float2 uv : TEXCOORD0;		//UV
				float3 tangent : TANGENT;	//タンジェント
			};

			// 頂点シェーダーからフラグメントに渡すもの
			struct v2f {
				float4 position  : SV_POSITION;
				float2 uv        : TEXCOORD0;
				float4 light	 : COLOR1;
			};

			//接空間への変換行列を取得
			float4x4 InvTangentMatrix(
   				float3 tangent,
   				float3 binormal,
   				float3 normal )
			{
				//接空間行列
				float4x4 mat = float4x4(float4(tangent.x,tangent.y,tangent.z , 0.0f),
								float4(binormal.x,binormal.y,binormal.z, 0.0f),
								float4(normal.x,normal.y,normal.z, 0.0f),
								float4(0,0,0,1)
                				 );
   				return transpose( mat );   // 転置
			}

			//頂点シェーダー
			v2f vert (v_in v)
			{
				v2f o;

				o.position = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = v.uv;

				float3 nor = normalize(v.normal);
				float3 tan = normalize(v.tangent);
				float3 binor = cross(nor,tan);

                o.light = mul(mul(unity_WorldToObject,_WorldSpaceLightPos0),InvTangentMatrix(tan,binor,nor));

				return o;
			}

			float4 frag(v2f i) : COLOR
			{
				float3 normal  = float4(UnpackNormal(tex2D(_NormalTex, i.uv)),1);
				float3 lightvec   = normalize(i.light.xyz);
				float  diffuse = max(0, dot(normal, lightvec));
				return diffuse * tex2D(_MainTex, i.uv);
			}

			ENDCG
		}
	}
}
								

Unityのシェーダーはまだ分からない部分が多いのでベースはUnityマニュアルを見て作りました。

Vertex and fragment shader examples

一旦全部空にするとこんな感じです。

Shader "Custom/bumpmap" {
  Properties {
  }
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
    Pass{
    }
  }
}

PropertiesにはUnityのInspecterでやりとりするための指定ができます。
今回は普通のテクスチャと法線マップをInspecterから渡せるようにします。

Properties {
  _MainTex("Texture", 2D) = "white" {}
  _NormalTex("NormalTex", 2D) = "white" {}
}

続いてShaderの中身はPassに書いていきます。

Pass
{

Cg/HLSL スニペット と呼ばれるものをコンパイルするために、最初に

CGPROGRAM

最後に

ENDCG

を書きます。

CGPROGRAMなどについてはUnityマニュアルのこちらに記載してありました。

シェーダー: 頂点とフラグメントプログラム

CGPROGRAMとENDCGの間にシェーダーを書いていきます。

今回は頂点シェーダーとフラグメントシェーダーを使うので

#pragma vertex vert
#pragma fragment frag

ビルトインシェーダーのヘルパー関数を使うため

#include "UnityCG.cginc"

テクスチャ、法線マップライト、ライトカラーを定義

uniform sampler2D _MainTex;
uniform sampler2D _NormalTex;

頂点シェーダーに渡すもの

struct v_in{
  float4 vertex : POSITION; //頂点座標
  float4 normal : NORMAL; //法線
  float2 uv : TEXCOORD0; //UV
  float3 tangent : TANGENT; //タンジェント
} ;

頂点シェーダーからフラグメントシェーダーに渡すもの

struct v2f {
  float4 position : SV_POSITION;
  float2 uv : TEXCOORD0;
  float4 light : COLOR1;
} ;

これであとはシェーダーを書いていくだけです。
まずは頂点シェーダー。

v2f vert (v_in v)
{
  v2f o;
  //頂点座標を射影空間へ
  o.position = mul(UNITY_MATRIX_MVP, v.vertex);
  o.uv = v.uv;

_WorldSpaceLightPos0はワールドのライト座標なのでワールドの逆行列と掛け合わせローカル座標に変換します。
ライトをフラグメントシェーダーに送るため、接空間の逆行列とローカル座標のライトを掛け合わせます。

 
  float3 nor = normalize(v.normal);
  float3 tan = normalize(v.tangent);
  float3 binor = cross(nor,tan);
  o.light = mul(mul(unity_WorldToObject,_WorldSpaceLightPos0), InvTangentMatrix(tan,binor,nor));

  return o;
}

InvTangentMatrix内では、接空間行列を作り転置して逆行列を作っています。

こちらは、その5 0から学ぶ法線マップを参考にしました。
なぜ接空間の逆行列を作るのかも解説されていますので参考にしていただければと思います。

float4x4 InvTangentMatrix(
  float3 tangent,
  float3 binormal,
  float3 normal )
{
  //接空間行列
  float4x4 mat = float4x4(float4(tangent.x,tangent.y,tangent.z , 0.0f),
  float4(binormal.x,binormal.y,binormal.z, 0.0f),
  float4(normal.x,normal.y,normal.z, 0.0f),
  float4(0,0,0,1) );
  return transpose( mat ); // 転置
}

続いてフラグメントシェーダー。

法線マップから法線を取り出します。

float4 frag(v2f i) : COLOR
{
  float3 normal = float4(UnpackNormal(tex2D(_NormalTex, i.uv)),1);
  float3 lightvec = normalize(i.light.xyz);
  //ライティングのみ反映した色
  float diffuse = max(0, dot(normal, lightvec));
  //テクスチャと合成
  return diffuse * tex2D(_MainTex, i.uv);
}

実装結果の画面がこちらです。

バンプマッピング結果

左が通常のテクスチャのみ貼り付けたもの、
右がバンプマップを実装したものです。

まとめ

今までDirectX9でシェーダを書いていたので、どうやって頂点や、ワールドマトリックス、ライト情報を渡しているのかわからず、Unityでシェーダーをどう描くかあまりイメージできていなかったのですが、調べてみると最低限必要な情報は簡単に取得できるように作られていて驚きました。

Unityでのシェーダーの書き方がようやく分かったので今後も色々なシェーダーに挑戦したいと思います。

明日は大原さんの記事です。

About the Author

中西裕

nknsytk

クライアントエンジニア

入社1年目のクライアントエンジニア
主な業務はCocos2d-xでアプリ開発をしています。
シェーダーにも興味があるのでUnityのシェーダーも書いています。