Help us understand the problem. What is going on with this article?

[Unity] ファーシェーダを移植してみた

More than 5 years have passed since last update.

こちらで実装されているファーシェーダの仕組みをUnityに移植してみました。

参考にしたほうがWebGLで実装されていて、動きもあってだいぶふさふさな感じです。
これをUnityでやれないかとあれこれ調べて、なんとか同じような感じになるようにしてみました。
だいぶ力技部分もありますが・・w
(余談ですが、WebGLコンテンツを参考にする場合はFirefoxのシェーダエディタが超絶便利です)

ちなみに作ったサンプルはGitHubに上げてあります。
動くサンプルもあります)

fur.png
作ったキャプチャ

[2014.09.24 追記]
バンプマッピングによるライティングを追加してみました。(サンプル
bump-sample.jpg

まずざっくり概要から

そもそもファーシェーダってなにかというと、3Dでふさふさな「ファー」を表現するシェーダです。
こちらのファーシェーダーの記事が概念を理解しやすいと思います。

実装方法や実現方法のアプローチはいくつかありますが、上記で書かれているのも今回やってみたのも同じアプローチのものです。

層を重ねる

記事の図を見てもらえれば分かりますが、ひとつのモデルを少しずつ膨らませて、何層にも分けてレンダリングするアプローチです。
要は 毛の断面図を何層も重ねたら毛っぽくなるよね ってことです。

記事では、バンプマッピングを使って毛にもライティングを施しさらにリアルにしていますが、今回はそこまではやっていません。

テクセルから色をどう取るか

ちなみに今回は以下のテクスチャを使いました。

kirin.png

このテクスチャは毛の断面ではありません。
なので断面になるようにする工夫が必要です。
そこで、以下のような断面っぽいテクスチャを別途用意します。

noise.png

さて、これをどう使うのか。
毛っぽく見せるわけですから、最初のテクスチャから色を取るところと取らないところを決めればいいわけです。
そうすれば毛の断面図のようなテクスチャとして使えるわけです。

ちなみに余談ですが、テクセルは「テクスチャのピクセル」でテクセルです。

フラグメントシェーダ

結論から言うと、 上記ふたつ目のテクスチャの中のテクセルが「ある閾値」以下の場合に破棄する という実装を行います。
(フラグメントシェーダで計算中のピクセルを破棄するにはdiscardを使います)

以下がフラグメントシェーダのコードです。
やってること自体はほんとにシンプルですね。

// フラグメントシェーダへの入力構造体
struct vert2frag {
    float4 position : POSITION;
    float2 uv       : TEXCOORD0;
    float2 uv2      : TEXCOORD1;
};

// ...中略...

// フラグメントシェーダ
float4 frag(vert2frag i) : COLOR {
    float4 map = tex2D(_SubTex, i.uv2);
    if (map.a <= 0.0 || map.b < FUR_OFFSET) {
        discard;
    }

    float4 color = tex2D(_MainTex, i.uv);
    color.a = 1.1 - FUR_OFFSET;
    return color;
}

フラグメントシェーダではふたつのテクスチャを参照しています。
_MainTex_SubTexです。

_MainTexは普通のテクスチャです。上記の画像では上部の色がついたほうです。
そしてキモとなるのが_SubTexで、白黒のほうのテクスチャです。

まず最初にサブテクスチャから、現在計算中のテクセルを取り出します。

float4 map = tex2D(_SubTex, i.uv2);

そしてその取り出したテクセル情報から、レンダリングすべきかどうかを判断する処理を行います。

if (map.a <= 0.0 || map.b < FUR_OFFSET) {
    discard;
}

アルファが0以下か、あるいは青成分(map.b)が特定の値以下だったらピクセルをレンダリングしない(discard)という意味です。

FUR_OFFSETはファーのオフセットで、今回は60層に分けてレンダリングしているので「層 / 60」の値が、層ごとに渡ってきます。
(例えば30層目だったら「30 / 60 = 0.5」)

バーテックスシェーダ

フラグメントシェーダは数行からなるだいぶシンプルな実装でした。
バーテックスシェーダのほうは若干複雑です。

まずはコードから。

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

// ...中略...

uniform float4 _Gravity;

vert2frag vert(vertInput v) {

    const float spacing = 0.35;

    vert2frag o;

    float3 forceDirection = float3(0.0);
    float4 position = v.vertex;

    // Wind
    forceDirection.x = sin(_Time.y + position.x * 0.05) * 0.2;
    forceDirection.y = cos(_Time.y * 0.7 + position.y * 0.04) * 0.2;
    forceDirection.z = sin(_Time.y * 0.7 + position.y * 0.04) * 0.2;

    float3 displacement = forceDirection + _Gravity.xyz;

    float displacementFactor = pow(FUR_OFFSET, 3.0);
    float4 aNormal = v.normal;
    aNormal.xyz += displacement * displacementFactor;

    float4 n = normalize(aNormal) * FUR_OFFSET * spacing;
    float4 wpos = float4(v.vertex.xyz + n.xyz, 1.0);
    o.position = mul(UNITY_MATRIX_MVP, wpos);
    o.uv  = v.texcoord;
    o.uv2 = v.texcoord2 * 20.0;

    return o;
}

まず、バーテックスシェーダへの入力として以下の情報を受け取ります。

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

頂点情報として位置と法線、ふたつのテクスチャのUV座標が渡ってきているわけですね。
: SV_POSITIONなどはセマンティクスと呼ばれています。この辺りは話がそれるので割愛します)

層のレンダリング

コードにはforceDirectionなる変数がありますが、このあたりは実際に動かした時に風にふかれているような演出をしているだけなのでなくても動きます。
キモとなる部分は以下の部分です。

float4 n = normalize(aNormal) * FUR_OFFSET * spacing;
float4 wpos = float4(v.vertex.xyz + n.xyz, 1.0);
o.position = mul(UNITY_MATRIX_MVP, wpos);
o.uv  = v.texcoord;
o.uv2 = v.texcoord2 * 20.0;

最初のnが、実際にモデルをふくらませるための係数になってます。
aNormalは上記の風を考慮した状態なので、単純にv.normalを使っても動きます)

法線情報に、ファーのオフセットとspacingという係数を掛けたものを、実際の頂点位置(v.vertex)に足しているわけですね。
このspacingの値を変えるとファーの層の間隔が変化します。(サンプルでは0.35)

注意点

最初、どちらもfloat4なのでそのまま足していました。

float4 wpos = v.vertex + n;

しかし、こうしてしまうとwの値がおかしくなり、モデルの形状がおかしくなってしまうので、xyzのみ計算して、wの値を1.0にする必要があります。

UV座標を計算

最後の部分がUV座標に関する計算です。
といっても、メインのテクスチャはそのまま、サブテクスチャのみ20倍しているだけですね。
この20倍している部分は別になくても問題ありません。単に、使用している断面のテクスチャの細かさを調整しているだけなので。
なので10倍や30倍などと値を変えると毛の雰囲気も変わります。

ちなみに5倍くらいにしたやつだとこんな感じになります↓(ふさふさ感割増)
fur2.png

複数Passで層をレンダリング

今回移植するにあたって悩んだところが、ひとつのシェーダでどうやって層をレンダリングさせるか、でした。
参考にしたWebGLの実装は、事前にモデルを60個生成して、それぞれレンダリングすることで実現していました。
ただ、Unityの場合はそうした実装ができません。

しかしUnityにはPassというコマンドがあり、このPassがまさにひとつのモデルをレンダリングする役割を持っています。
なので、今回の実装が力技だと書いたのは、このPassを単純に60回書いているからです。

Passの例

Pass {
    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag

    #define FUR_OFFSET 0.016

    #include "FurHelper.cginc"

    ENDCG
}

// 以下、0.016ずつ増えるPassを60回書く

この、#define FUR_OFFSET 0.016となっている部分が微妙に異なるPassを60回書いている、というわけですね。(他にいい方法あったら教えて下さいorz)
(もちろん、実際は60回分のPassを書き出してくれるプログラムを別に作ってますが・・)

ちなみにこのPassを複数回書くヒントはUnityのForumで話されていたものを参考にしました。

だいぶ冗長な書き方ではありますが、とりあえずUnity上で動くものが作れたのでよしとしますw

Unity AssetStoreに並びました

fur-shader-sample.png

Simple Fur Shader | Unity Asset Store

※ AssetStoreのものはさらに、 通常のライティングとバンプマッピングによるライティングを施したもの となっています。

KAYACのLPでも使ってます

KAYACのWebGLサイト制作募集のサイト
ここでもファーシェーダが使われています。実装方法はほぼ同じ。
こちらのサイトではさらにいくつかのシェーダを切り替えてロゴの見た目を変えられるようになってます。
iOS8なら実際に動いているところが見れるので見てみてください( *'-')

kayac.png

edo_m18
現在はUnity ARエンジニア。 主にARのコンテンツ制作をしています。 趣味でWebGL/WebXRもいじってます。 Unityに関するブログは別で書いています↓ https://edom18.hateblo.jp/
http://edom18.hateblo.jp/
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした