てつぶろぐ

けつぶろぐ。

知見のゴミ箱。

クソゴリラ調教日誌:ファーシェーダ

f:id:honzyou1753:20180306215909p:plain

はじめに

お久しぶりです。ハンドルネーム増やしすぎて自分を見失っているチンパン28号です。
何百番煎じかわかりませんが、最近バーチャルユーチューバーとやらを始めました。


クソゴリラ、自己紹介する.002

その中でニシローランドゴリラの毛並みを再現するためにファーシェーダを書いてたので、知見として残しておきます。

ファーシェーダはその名の通り体毛を表現するためのシェーダです。
生物の毛並みを表現する手段としては、モデリングの段階で体毛を束で生やしていく方法もありますが、クソゴリラの全身に生えている毛をモデリングするのは非情に手間がかかる上に、
束ごとに作るとどうしてもアニメ調になってしまいがちです。

アニメキャラを作る場合には効果的ですが、今回は東山動物園のイケメンゴリラシャバーニくんを目指しているのでファーシェーダ、書いていきましょう!

説明の内容は、ShaderLabの頂点・フラグメントシェーダの基本をおさえている人向けです。

使用エンジン:Unity
使用言語:ShaderLab

参考にしたサイト様

ファーシェーダの理屈を知る上でこの記事が個人的には一番参考になりました。
Maverick Project

ShaderLabの記法で参考になったのはこちらの方の記事です。
純粋にファーシェーダだけ実装したいならこっちの記事のほうが分かりやすいかも……
.cgincの存在はこの記事で知ったので、ガッツリパク…参考にしてます
qiita.com

簡単に説明すると、元となるモデルの毛を生やしたい面にテクスチャの透明度を変えた面を何層も重ねるティラミス方式で毛を表現するのがファーシェーダの理屈です

材料

  • むき身のクソゴリラ一匹

f:id:honzyou1753:20180306230740p:plain:h256

  • クソゴリラボディのメインテクスチャ
    • 載せているのはファーシェーダを適用する身体のみですが、顔も用意しています

f:id:honzyou1753:20180306230911p:plain:h256

  • 毛のばらつきを表現するためのノイズテクスチャ
    • GINP2のフィルタで簡単に作れます
    • ノイズを細かくすることでふわっとした毛になり、荒くすることでゴワゴワの毛になります

f:id:honzyou1753:20180306231538p:plain:h256

  • 毛の生える量をUV座標で指定するための明度テクスチャ
    • 白いほど毛が長く、真っ黒は無毛です
    • UV座標はメインテクスチャに準拠します

f:id:honzyou1753:20180306232452p:plain:h256

  • トゥーンシェーダ(太陽光反映)用の色味指定用画像

f:id:honzyou1753:20180306232359p:plain

実装

ここわかんねぇぞ解説しろ!って部分があったらコメント下さい

Fur.shader

Shader "Custom/fur" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}	//  メインテクスチャ
		_FurTex  ("Base (RGB)", 2D) = "white" {}		// ファー用ノイズテクスチャ
		_FurMapTex  ("Base (RGB)", 2D) = "white" {}	//  ファーの生える位置・量を決める白黒テクスチャ
		_Ramp ("Toon Ramp (RGB)", 2D) = "gray" {} 	// トゥーンシェーダ用テクスチャ
		_Gravity ("Gravity", Vector) = (0.0, 1, 0.0, 0.0)	// 重力、ココいじれば毛を逆立てたり垂らしたり出来ます
		_FurLength ("Length",float) = 1			// 毛の長さ(計算的には掛け算をしているので名前あってないなこれ)
	}
	
	Category {
		Tags {"Queue" = "Transparent"}
		Blend SrcAlpha OneMinusSrcAlpha
		// パスは1+30で固定
		SubShader {
		// 一番最初は地肌を描画する
		Pass {
			CGPROGRAM
				
			#define FUR_OFFSET 0.0
			#include "UnityCG.cginc"
			#include "AutoLight.cginc"
                	#include "FurHelper.cginc"

			#pragma vertex vert
			// 最初だけ地肌用のフラグメントシェーダ使ってます
			#pragma fragment skinfrag
                
			ENDCG
            	}
		// 以降はひたすら体毛の層を重ねる
		Pass {
			CGPROGRAM

			#define FUR_OFFSET 0.01

			#include "UnityCG.cginc"
			#include "AutoLight.cginc"
                	#include "FurHelper.cginc"

			#pragma vertex vert
			#pragma fragment furfrag
                                        
                	ENDCG
			}
		}

		// 以降はFUR_OFFSETの値だけ少しずつ増やして、ひたすら残り29Passを書いてます
		// FUR_OFFSETの値は透明度から引くので0~1の間でよしなに設定しましょう

		Fallback " VertexLit", 1
	}
}
ポイント解説
Category {
	Tags {"Queue" = "Transparent"}
	Blend SrcAlpha OneMinusSrcAlpha

Category {}の先頭で設定したTagsやアルファブレンドなどの設定は、以降のCategory {}で囲んだ全てのPassで適用されます。便利。

	#define FUR_OFFSET 0.0
	~
        #include "FurHelper.cginc"

FUR_OFFSETはFurHelperで使うので、先に書いとかないとエラー吐きます。

FurHelper.cginc

.cgincファイルは、.shderファイルに#includeすることで一つのシェーダー関数を使い回すことが出来ます。
今回のような複数Passを何度も書くような場面で凄い便利でした。

#ifndef EDO_FUR_SHADER_HELPER
#define EDO_FUR_SHADER_HELPER

// 頂点シェーダへの入力構造体
struct vertInput {
	float4 vertex    : POSITION;
	float4 normal    : NORMAL;
	float2 texcoord  : TEXCOORD0;
};

// フラグメントシェーダへの入力構造体
struct vert2frag {
	float4 position : POSITION;
	float3 normal   : TEXCOORD0;
	float2 uv       : TEXCOORD1;
	float2 furUv      : TEXCOORD2;
	float3 lightDir : TEXCOORD3;
	half glay		: TEXCOORD5;
	float4 vertex	: TEXCOORD6;
};

uniform sampler2D _MainTex;
uniform sampler2D _FurTex;
uniform sampler2D _FurMapTex;
uniform sampler2D _Ramp;
uniform float4 _Gravity;
uniform float _FurLength;

// 頂点シェーダ
vert2frag vert(vertInput v) {
	vert2frag o;
	o.glay = tex2Dlod (_FurMapTex, float4(v.texcoord.xy,0,0)).r;
	
	// 変位係数で毛の位置を散らす
	float displacementFactor = pow(FUR_OFFSET, 3.0);
	float4 normal;
	normal = normalize(v.normal + _Gravity * displacementFactor);
	
	float4 vertexOffset = normal * FUR_OFFSET * o.glay * _FurLength;
	float4 worldPos = float4(v.vertex.xyz + vertexOffset.xyz, 1.0);
	o.position = UnityObjectToClipPos(worldPos);
	o.uv  = v.texcoord;
	o.furUv = v.texcoord * 10.0;

	o.vertex = v.vertex;
	o.normal = normalize(v.normal).xyz;
	o.lightDir = normalize(ObjSpaceLightDir(v.vertex));

	return o;
}

// フラグメントシェーダ
float4 furfrag(vert2frag i) : COLOR {
	float4 map = tex2D(_FurTex, i.furUv);
	if (map.r < FUR_OFFSET || i.glay <= 0.0) {
		discard;
	}

	fixed atten = LIGHT_ATTENUATION(i);
	// 以下はトゥーンシェーダ
	// 光源の向きと頂点法線の内角から0~1の値を取って、トゥーン輝度割当用の画像のx,y軸に当ててるだけです
	half d = dot (i.normal, i.lightDir)*0.5 + 0.5;
	half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
	float4 color = tex2D(_MainTex, i.uv);
	color.a = 1.1 - FUR_OFFSET;
	return color * float4(ramp,1) * (atten * 2);
}
// 地肌用フラグメントシェーダ
// _FurTexの値で描画判定をしている以外は同じです
float4 skinfrag(vert2frag i) : COLOR {
	fixed atten = LIGHT_ATTENUATION(i);
	half d = dot (i.normal, i.lightDir)*0.5 + 0.5;
	half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
	float4 color = tex2D(_MainTex, i.uv);
	color.rgb = color * ramp * (atten * 2);
	return color;
}

#endif
ポイント解説
o.glay = tex2Dlod (_FurMapTex, float4(v.texcoord.xy,0,0)).r;

毛の生える長さの比率を_FurMapTexの各UV座標の明度から0(ハゲ)~1(ふさふさ)で取っています
頂点シェーダでUV座標を取る場合はtex2Dlod を使用します。
今回は_FurMapTexをグレースケールで作ったので適当にr(赤)の値を取っていますが、ちゃんと明度を取りたいにしたいって人は
glay = r * 0.3 + g * 0.6 + b * 0.1;
で彩度付き画像からグレースケールを取れるらしいですよ。正直画像側から設定したほうが楽だし計算コストもかからないから推奨しないけど。

	float4 n = normalize(aNormal) * FUR_OFFSET * o.glay * _FurLength;
	float4 wpos = float4(v.vertex.xyz + n.xyz, 1.0);
	o.position = (UNITY_MATRIX_MVP, float4(wpos, 1.0));

ここでは毛の伸びる方向と長さから頂点の最終的なワールド座標を決めています
UNITY_MATRIX_MVPでモデルビュー行列×射影行列を既に定義してくれてます。Unity便利。

o.furUv = v.texcoord * 10.0;

ここではファーのノイズテクスチャを適用するUVを元のUVの拡大して代入しています。
なぜ拡大しているかというと、そのままUVを適用すると毛の一本一本がぶっとくなるからです。
一応かける値の増減でコード側から毛の粗さをいじれます

float4 map = tex2D(_FurTex, i.uv2);
if (map.a <= 0.0 || map.r < FUR_OFFSET || i.glay <= 0.0) {
	discard;
}

ノイズ画像の明度(r)がFUR_OFFSETより小さいか、頂点シェーダ側で設定した_FurMapTexの明度が0であれば描画をしないようにしてます。
正直、FUR_OFFSETじゃなくて別途定数を用意したほうが良いと書いてて思ったので、みんなはそうしてね。

これにはクソゴリラもニッコリ

f:id:honzyou1753:20180307014936p:plain

最後に

ファーシェーダは複数パスに変更が及ぶ結構重い処理なので、Unityがまれに落ちます。
セーブはこまめにしましょう(戒め)。

一つのPassの中でfor文でかっこよく回せないかなと思って、色々調べてたけど結局できなかったなぁ…
もしくは Pass間で値を受け渡す方法知りたい。



あと

はてなコード書きづらすぎだろ!!!!!!!!!!!!!!!
というわけで次回はQiitaでお会いしましょう。はいウホウホ(ノルマ達成)

============= 2018/05/13 追記 =============
記事内に誤りがありました。
> ファーシェーダは複数パスに変更が及ぶ結構重い処理なので、Unityがまれに落ちます。
> セーブはこまめにしましょう(戒め)。
と言っていましたが、実際に落ちる原因はフラグメントシェーダの戻り値にfixed4(8bit)を指定していた時があり、
256以上の色データが返された際にオーバフローを起こしていただけでした。
載せてあるサンプルコードでは直っていたので、影響はないです。
とは言ってもfloat4でも無駄があるので、halfが適切かと思います。