Hiromuブログ

最近はこちら(https://zenn.dev/hiromu)が本体

ファーシェーダを試してみた話

ファーシェーダとはモフモフを表現するためのものですが、そこそこ重い処理なのでHoloLensでどうなるか試してみました。

fur


結論、UnityデフォルトのSphere2つ、Plane1つに60パスのシェーダを適用したところ10FPS前後しか出ませんでした。利用するにはパス数と見た目の妥協点をさぐる必要がありそうです。


ファーシェーダ実装

今回試した手法は以下で紹介されているものですが、サンプルが動かなかったので動くようにし、(自分的に)少し便利にしたものです。

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

上記で肝となるのは「複数Passで層をレンダリング」しているところです。

この部分を少し工夫してエディタ拡張でメニューから操作し、何層にするかを指定できるようにしました。

fur01


ここで例えばpassを30としてCreateボタンを押すと、Assets/Shaders配下にPassが30個記載されたFur.shaderが生成されます。

fur02


あとは、新規マテリアルを作成し、生成されたCustom/FurShaderを割り当て以下のような設定をします。

(メインテクスチャはPIXELSから探し, サブテクスチャはPhotoshopのノイズフィルタで作成)

fur03


このマテリアルを任意のオブジェクトにアタッチすればモフモフを確認することができます。


このFur.shaderの自動生成はあらかじめシェーダのテンプレートを用意し、それをもとに生成しています。具体的なテンプレートおよびスクリプトは以下になります。

シェーダのテンプレート(Assets/Resources配下にFurTemplate.txtとして保存)

Shader "Custom/FurShader" {
    Properties{
        MainTex("Main Texture", 2D) = "white" {}
        SubTex("Sub Texture", 2D) = "white" {}
        Gravity("Gravity", Vector) = (0.0, -0.75, 0.0, 0.0)
        Spacing("Spacing", float) = 0.35
        _Preciseness("Preciseness", float) = 5
    }

CGINCLUDE
#include "UnityCG.cginc"

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

// 頂点シェーダの出力およびフラグメントシェーダへの入力構造体
struct v2f
{
    float4 position : SV_POSITION;
    float2 uv       : TEXCOORD0;
    float2 uv2      : TEXCOORD1;
};

uniform sampler2D _MainTex;
uniform sampler2D _SubTex;
uniform float4 _Gravity;
uniform float _Spacing;
uniform float _Preciseness;

v2f FurVert(fixed FUR_OFFSET, appdata v)
{
    v2f o;
    float3 forceDirection = float3(0.0, 0.0, 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 = UnityObjectToClipPos(wpos);
    o.uv = v.texcoord;
    o.uv2 = v.texcoord2 * _Preciseness;

    return o;
}

float4 FurFrag(fixed FUR_OFFSET, v2f i)
{
    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;
}
ENDCG

Category{
    SubShader{
        ##PASS##
    }
}

Fallback " VertexLit", 1

}

エディタ拡張スクリプト(Assets/Editor配下にCreateFurShader.csとして保存)

using System;
using System.IO;
using System.Text;
using UnityEngine;
using UnityEditor;

/// 
/// ファーシェーダファイルを生成するエディタ拡張
/// 
public class CreateFurShader : EditorWindow
{
    /// 
    /// シェーダのパス数
    /// 
    [Range(1, 100)]
    public static int passCount = 30;

    /// 
    ///  インデントスペース
    /// 
    private const string INDENT12 = "            ";
    private const string INDENT16 = "                ";

    /// 
    /// メニュー名
    /// 
    private const string _menuName = "SampleMenu/CreateFurShader";

    /// 
    /// ファーシェーダの入力テンプレートファイル名
    /// 
    private const string _importPath = "Assets/Resources/FurTemplate.txt";

    /// 
    /// 出力ファイル名
    /// 
    private const string _exportPath = "Assets/Shaders/Fur.shader";

    /// 
    /// エントリポイント
    /// 
    [MenuItem(_menuName)]
    public static void ShowWindow()
    {
        // すでにWindowSampleが存在すればそのインスタンスを取得し、なければ生成する
        var window = EditorWindow.GetWindow(typeof(CreateFurShader));
    }

    /// 
    /// GUIを描画する
    /// 
    private void OnGUI()
    {
        // ラベルの表示
        GUILayout.Label("Fur Settings", EditorStyles.boldLabel);

        // パス数
        passCount = EditorGUILayout.IntSlider("pass", passCount, 1, 100);

        // シェーダファイル生成
        if (GUILayout.Button("Create"))
        {
            string str = CreateShaderString(_importPath);
            if (str != "")
            {
                OutputFile(_exportPath, str);
            }
            else
            {
                Debug.LogWarning("error");
                return;
            }

            // 完了ダイアログ表示
            EditorUtility.DisplayDialog("確認", _exportPath + "を生成しました", "OK");
            Debug.Log(_exportPath + "を生成しました");

            // パスを指定してインポートする
            AssetDatabase.ImportAsset(_exportPath);
        }
    }

    /// 
    /// テンプレートファイルをベースにシェーダの文字列を生成する
    /// 
    /// テンプレートファイル
    private static string CreateShaderString(string _filePath)
    {
        FileInfo fi = new FileInfo(_filePath);
        string resultString = "";
        try
        {
            using (StreamReader sr = new StreamReader(fi.OpenRead(), Encoding.UTF8))
            {
                while (sr.EndOfStream == false)
                {
                    string line = sr.ReadLine();
                    if(line.Contains("##PASS##"))
                    {
                        for(int i=0; i < passCount; i++)
                        {
                            resultString += INDENT12 + "Pass{ // PassCount " + i + "\r\n" +
                                            INDENT16 + "CGPROGRAM" + "\r\n" +
                                            INDENT16 + "#pragma vertex vert" + "\r\n" +
                                            INDENT16 + "#pragma fragment frag" + "\r\n";
                            float offset = (float)i / (float)(passCount - 1);
                            resultString += INDENT16 + "#define offset " + offset + "\r\n";
                            resultString += INDENT16 + "v2f vert(appdata v) { return FurVert(offset, v); }" + "\r\n" +
                                            INDENT16 + "float4 frag(v2f i) : COLOR{ return FurFrag(offset, i); }" + "\r\n" +
                                            INDENT16 + "ENDCG" + "\r\n" +
                                            INDENT12 + "}" + "\r\n";
                        }
                        continue;
                    }
                    resultString += line + "\r\n";
                }
            }
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
        }
        return resultString;
    }

    /// 
    /// シェーダファイルを生成する
    /// 
    /// 
    /// 
    private static void OutputFile(string _filePath, string str)
    {
        try
        {
            using (StreamWriter sw = new StreamWriter(_filePath, false))
            {
                sw.WriteLine(str);
            }
        }
        catch(Exception e)
        {
            Debug.Log(e.Message);
        }
    }
}


なお、ジオメトリシェーダを使えばもっとエレガントな実装ができるかもしれませんが実行環境が対応しているかどうか確認する必要があります。

参考

ファーシェーダの概要については以下の記事が分かりやすかったです。

ジオメトリシェーダのアクセラレーション的活用(2)~ファーシェーダを加速する/その1~フィン法のファーシェーダ
ジオメトリシェーダのアクセラレーション的活用(3)~ファーシェーダを加速する/その2~シェル法のファーシェーダ


</fur_offset)>