先月新しいOS(Win10)に変わったあと記事の更新がうまくいかなくて弱っていましたが、なんとか解決できたようで新規の記事投稿ができました。直接ブログのほうに書くことはできたんですけど量がある場合エディタを通して転送しないとちょっと無理なのでした。

 

今回の記事のネタ元はこちらの動画。関連動画で扱ったプロジェクトファイルを配布されているのですが、この動画のシーンファイルは入っていないんです そこでUnityで実装テストをしてみましたというのが今回の趣旨です。 後ほど作成したシーンファイルをアップロードします時間がなくてまだコードをつめてありませんが  しばらくお待ちください。

一応コード内のコメントが動画中のUE4シェーダノードのコメントに対応させてあります 変数名も合わせてありますので参考にしていただければ理解がし易いかと思います。

 

 

 

ProcTest

 

未使用な変数があって警告が出ますが、拡張予定の部分なので動作には影響ありません 無視してしまってください。




■スクリプトの解説

参考リンクをもとに解説します。

 

難しい部分は特に無いと思いますが 内容はメッシュインスタンシング描画の指定と シェーダへの値のセットだけです。

メッシュの横方向の並び数と縦方向の並び数をシェーダにセットしています。参考元のスクリプトではバウンディングボックスを10000と再計算させていますがこれはカメラのカリングエリアから遠くなっても描画されるための設定ですので、除外してください。

meshes[0].RecalculateBounds(); で一度インポートしたメッシュのバウンディングボックスを再計算してサイズをメッシュモデルにフィットさせてからサイズを取り出してしシェーダにサイズをモデルを配置するオフセット値として渡します これで正確にメッシュを並べて描画できます。

Shader.SetMatrix()などの追加されたセット命令は内部的にはコンスタントバッファを使用してデータの転送が行われるため コンスタントバッファのストライド長、DirectXの場合1024バイト制限があるのでデータ量が多くなると足りなくなると思います。それ以上量のデータを扱う場合はComputeBufferかまたはテクスチャアレイの使用をすることになると思いますので、ComputeBuffer colorBuffer 関連をコメントアウトしましたが あとで使用することになると思うので そのような理由で残してあります。

それからスクリプトの以下の部分の注意事項で 複数メッシュモデルを同時にインスタンシングで描画したときにシャドーが最初のメッシュモデルのものしか反映されないという バグの回避策として提案されているものでmpbs[i].SetFloat("_Bla", (float)i); マテリアルプロパティにダミーIDの値をセット することで複数メッシュのインスタンシング描画でも影が表示されるというTips的なものです。元スレではver5.4.4ではまだ修正されていないようです 最新バージョンの確認をしていないため正確にはわからないのですが 旧バージョン対応のため記述しておいたほうがよさそうです。

 

   /// this is the magic line. Uncomment this for shadows!!
                mpbs[i].SetFloat("_Bla", (float)i);
                if (render[i])
                    Graphics.DrawMeshInstancedIndirect(meshes[i], 0, materials[i], meshes[i].bounds, argsBuffers[i], 0, mpbs[i], castShadows, receiveShadows);

 

 

“nstancedProcedualBuild.cs”

 

■シェーダの解説

"InstancedProcedualBuidShader.shader”

 

#pragma instancing_options procedural:setup  を記述することで void setup() {}内でインスタンシング描画前のセットアップセッティングを行うことができます。 c#スクリプトにおけるStart()関数のようなものだと考えてください。マニュアルのサンプルを見るとわかるようにシェーダ内で使用する変数や配列を定義しています

ワールド座標変換に使用するunity_ObjectToWorld配列にインスタンシングメッシュのスケールとワールド座標を上書きで指定しています。 今回の場合はインスタンスメッシュごとにrandom変数を設定する必要があるのでここで計算してRandomNoise 変数にセットしています。RandomNoise変数はどのシェーダからも呼び出せるようにシェーダパスの外で宣言します。

unity_InstanceIDの値がインスタンスごとにシェーダに与えられるユニークな固有IDですのでこの値をシードに取ることで各インスタンシングメッシュのランダム値に変化を与えています。

 

■ rand ( )

rand()はランダムノイズを返す関数です。以下のサンプル、ランダム関数の上2つは、ほぼ同じ式で引数のシードがfloat2型かfloat3型の違いです、UV値などをもとにランダム値を取りたい場合float2型の引数をとる関数を使用して ワールド座標などfloat3型からランダム値を得たいときはfloat3型の引数をとる関数を作成してシード値とすればよいわけです 両方記述してオーバーロードさせるなど用途に応じて実装します。 ランダム値はシードが同じならば常に同じ値が返りますが、

参考として おなじみの?メルセンヌツイスター法などリンクも貼りましたが 複雑さが増すほど処理は重たいので、ケースに合わせて実装してみてください。

    float rand(in float2 uv)
      {
          float2 noise = (frac(sin(dot(uv ,float2(12.9898,78.233)*2.0)) * 43758.5453));
          return abs(noise.x + noise.y) * 0.5;
      }


    float rand(in float3 pos)
      {
         return frac(sin(dot(pos, float3(12.9898, 78.233, 45.5432)*2.0)) * 43758.5453);   
      }

 

     float rand(float3 myVector) {        

          return frac(sin(_Time[0] * dot(myVector, float3(12.9898, 78.233, 45.5432))) * 43758.5453); 
        }

     uint rand_xorshift()
    {
        // Xorshift algorithm from George Marsaglia's paper
        rng_state ^= (rng_state << 13);
        rng_state ^= (rng_state >> 17);
        rng_state ^= (rng_state << 5);
        return rng_state;
    }

 


■ バーテックスカラーによる制御

 

vertColorMesh

 

メッシュモデルごとの 頂点カラーのペイントでテクスチャの合成比率、頂点の移動などの制御を行います。カラー強度で更に細かく合成の度合いなどのコントロールをすることができます。 頂点カラーペイントは各DCCツールごとに用法を参照してくだい。

 

今回の頂点カラーRGBそれぞれの用途は以下のようになっています。

R: 2色設定する壁のカラーのブレンドで_WallPaintColor1がベースカラーで_WallPaintColor2が頂点カラーRの値で塗り分けられる部分になります。

G: カラーパレットの指定をします。今回の場合は干されている服のカラーチェンジに使用されていて座標に応じてランダムにカラーパレットの読み出し座標を頂点カラーの値で変化させることができます。

B: メッシュの移動とオペーク=表示非表示の指定に使用します。メッシュモデルのローカル座標系のx方向のランダム移動を行い しきい値_HidingThresholdを越えた場合UE4ではオペークの値をセットしていますが、今回はDiscard命令でフラグメントシェーダでの描画をスキップさせています。

 

基本的な考え方はテクスチャを通常時とDirt(汚し)の2種類作成して、アルファチャンネルおよびノイズテクスチャで合成していくことで、メッシュ表面のカラーに変化を与えます。実装方法に関しては、あとで記述しているWorld_Aligned_Cell_Noise ( )で解説しています。今回の場合BASE_COLORテクスチャのDirtのアルファチャンネルが汚し部分の合成に使用されます。 汚し用のDirtテクスチャは最終的にProcTest_Mud_Blendテクスチャ泥のカラーを使用して壁テクスチャのカラーと合成されます。

スペキュラ、ラフネス、アンビエントオクルージョンはグレースケールデータですので別々にせず一枚のテクスチャのRGBチャンネルに格納してしまいます。 今回は手抜きですが通常時とDirtを同じテクスチャを使用しています 絵的にはそれほど影響はないのですがAOくらいは差し替えてもいいかな。 ノーマルマップも通常とDirtの2種類を用意して同様に合成します。

MainTextureのスロットは未使用ですが UV値をuv_MainTexで統一した関係で残してあるだけですので使いやすく変更してください。

シェーダモデル3.0 (DirectX9)の場合はテクスチャのスロット数は上限が8になるので 今回のテクスチャ数だと入り切らないかもしれませんが

テクスチャをアトラス化して複数のテクスチャをパックしてしまうか あるいはノーマルマップなどはBとAの2チャンネルが空いているのでグレースケールのテクスチャであればそこに収めてしまう作業が必要です。インスタンシング描画はシェーダモデル4.0以上の対応ですので、メッシュインスタンシグを使用できる環境であれば問題にはならないでしょう。

 

※文字情報だとイメージが湧きにくいかもしれませんので テクスチャ画像を添付しておきます。

 

■ベースカラー

ProcTest_Base_Color_work

■スペキュラ+ラフネス+アンビエントオクルージョン

AORoughSpec

■Mud_Blend

ProcTest_Mud_Blend











■ノーマルマッピング

ProcTest_Normal_work

■シェーダInspector

inspector

 

■ World_Aligned_Cell_Noise ( )

UE4のWorld_Aligned_Cell_Noise ( )に相当する関数はtriplanar マッピングで代用します。TRIはラテン語で3ですのでxyz3方向のプレーンからマッピングを行う手法ということになります。 オブジェクトがワールド空間に配置された時点のXY平面,YZ平面、ZX平面のワールド座標をUVに見立てて

それぞれのプレーンに割り当てたテクスチャからカラーを取得しますが どのような形状でもテクスチャは破綻なくフィットするので主に背景モデル 、テレインなどに使用することが多いです。UVを生成する必要が無いためプログラマブルに作成したモデルにテクスチャを反映するときにも有効です。例えばガレキをスクリプトでランダム生成したとしてそこにテクスチャを張り込むと言った使用法も考えられます。

今回はノイズテクスチャの値ををtriplanar マッピングを使用してワールド空間座標から取得しています。ノイズテクスチャはグレースケールで使用しますので1チャンネルあれば格納できます。テクスチャはRGBAの4チャンネルがありますのでどのチャンネルでも自由に配置してください。

Noise関数は自前で実装してもよいのですが、リアルタイムに使用するには計算コストが高いのでゲーム全体に使用する場合かなり処理負荷が上がってしまいます、実行時にリアルタイムに変化させる用途以外にはパラメータが確定したものからテクスチャレンダーでテクスチャー化してしまうなど なるべくテクスチャの参照で済ませてリアルタイムの演算を避けるほうがベターだと思います。

ノイズテクスチャはフォトショップのフィルタやサブスタンス系のアプリケーションで生成するか 、場合によって画像検索で適切なものを探すなどでもいいと思います。fig2 のように空きチャンネルに fig2右のようなノイズテクスチャを必要な分登録して使いまわしていく感じですが、シェーダのコードでスケールやタイリング、オフセットをずらす 複数のテクスチャカラーをブレンドして変化を与える などと計画的に使用すれば少ないテクスチャ数でも十分用途に耐えると思います。

 

【fig1】

 

http://evgeniyzaitsev.com/2012/01/29/triplanar-uv-mapping-shader-in-udk/

【fig2】

mask_tumblr_inline_moub87xZOS1qz4rgp

 

  

  float4 World_Aligned_Cell_Noise(float3 worldPos, float3 worldN, float3 textureScale)
    {
       // Calculate a blend factor for triplanar mapping.
        //float3 worldN = UnityObjectToWorldNormal(IN.localNormal);
        float3 bf = normalize(abs(worldN));
        bf /= dot(bf, (float3)1);

        // Get texture coordinates.
        float2 tx = worldPos.yz * textureScale.x;
        float2 ty = worldPos.zx * textureScale.y;
        float2 tz = worldPos.xy * textureScale.z;

        // Base color
        fixed4 cx = tex2D(_NoisePaintedTex, tx) * bf.x;
        fixed4 cy = tex2D(_NoisePaintedTex, ty) * bf.y;
        fixed4 cz = tex2D(_NoisePaintedTex, tz) * bf.z;
        fixed4 WorldAlignedTextureCol = cx + cy + cz;

        return WorldAlignedTextureCol;

    }

簡単にコードの解説をすると triplannerマッピングは bf(ブレンドファクター)に ワールド法線を求めて

float3 worldN = UnityObjectToWorldNormal(IN.localNormal) でバーテックスシェーダから取得するローカル座標系の頂点法線をワールド空間座標系に変換します。プレーン面のベクトルはそれぞれx方向のプレーン面(1,0,0) y方向のプレーン面(0,1,0) z方向のプレーン面(0,0,1) となります つまり(1,1,1)はxyz3ではxyz方向のカラーが3分の1づつブレンドされれば良いわけです ワールド法線とfloat3(1,1,1)ベクトルではこのベクトルとの内積を取りその値で割り算することでブレンドファクターが求まります。

 

■ MF_TAA_Remap_01_Clamped ( )

 

UE4  Good-Looking Randomization for Procedural Bui.mp4_20170911_190256.061 UE4  Good-Looking Randomization for Procedural Bui.mp4_20170911_190246.725
    float MF_TAA_Remap_01_Clamped (float value, float min, float max)
    {
        return (value - min) / (max - min);
    }

画像のように、Min、Max, value を与えることでMin、Maxを01範囲にクランプ処理してValueを加工して返すカスタム関数ですが 元の動画との兼ね合いでのわかりやすさを考えてシェーダコードでは一応定義しました、これはSmoothstep()関数と同様の動作だと思いますのでSmoothstep()関数に置き換えてしまってもいいと思います。

 

■ ColorPalette

Noise_Palette

頂点カラーG成分で洗濯物のメッシュカラーをコントロールします。 カラーはカラーパレットテクスチャーから取得します。

シェーダでテクスチャー配列やテーブルを参照する場合は、できる限り横方向(U方向)に長いテクスチャーを作成するようにしましょう UVのV成分の計算を省略できてわずかながら処理を稼ぐことができます。

  float2 PaletteUV = float2((IN.VertexColor.g + RandomNoise) * ColorPaletteTiling, 0.5);

ColorPaletteTilingはカラーパレットは8色指定していますが 何色(数)のカラーを使用するかのエリアを決定する値です VertexColor.g はUV値のU成分テクスチャカラーの読み出す位置のオフセット成分になります。 V=0.5はテクスチャのY座標の中間部分を参照しています 端部分を指定すると今回の例では問題になりませんが、テクスチャ補完で隣接したピクセルから予想外のカラーを拾ってしまう場合もありますので 気をつけてください。

 

 

■追記

GPUでは分岐命令、例えばifステートメントなどのSIMD命令はシェーダ関数以外にあとから追加されている命令はシェーダ内で多用すると処理負荷が上がるというのを聞いたことがあるかもしれません。 その最適化は様々ありますが

if (A > HidingThreshold || A == HidingThreshold)  OpacityMask = 0; 
     else  if (A < HidingThreshold)  OpacityMask = 1;

もとのUE4のマテリアルノードを参考にするとこのような記述になりますが、オーソドックスな方法としてStep()関数を使用してif分岐を回避することができます。

step(x,y) 式 
 x<yのとき 式がTrue

:Opacityを透明フラグと考えると透明キュー使用となるため処理低下の原因になりそうですので UnityでのOpacityフラグは表示のONOFFと捉えて

フラグメントシェーダ内でdiscard命令を使用することにします。discardはフラグメントシェーダをピクセル数分のforeach()ループと捉えた場合break文に相当すると考えてください。

step()関数を使用してコードをリライトすると以下のようになります。

step (A , HidingThreshold ) discard;

IFステートメントはUnity内で最適化されるので 通常使用には問題にならないと思いますが、ループ内で使用したり頻繁にアクセスする関数に使用したりする場合など強制的にコードをコントロールする場合の知識として覚えておく程度でかまわないと思います。

 

あとは今回手抜きしてしまったのですが、Unite2016で話のあった Shader.PropertyToID( "変数名" ) でマテリアルプロパティから変数IDを取得して

シェーダにIDを使用して値をセットするほうが2倍程度早いというTipsですね。多くのシェーダに値を転送する場合は必要になりそうですが。データの更新時に一回しか転送しないので今回のケースだとあまり効かないかもしれません。そのため変数名で直接値をマテリアルにセットしてあります。

そこら辺は気がついた人は環境に合わせて直していただければよろしいかと思います。。。 

■ material.SetColor( "_Color", Color.white );

を以下のように変更するやり方ですね

■ var nameId = Shader.PropertyToID( "_Color" );

material.SetColor( nameId, Color.white );

 

参考の動画の最後で触れているデカール処理について それとインスタンシング処理に関する解説は、記事に入り切らなかったので、次回解説していきます。

デカールシェーダはディファードベースはすでにUnity公式から提供されていますので、フォワードベースの方法でいくつかを 間に合わなければ一つ。

過去にシェーダでの配列の解説をしてきたので、その延長記事になります。 マテリアルを一つにまとめられるのでスタティックバッチ処理が走ってもドローコールにはほぼ影響がないはずです。が何か見落としがあるかもしれません。

 

というところで わからないところがあればコメントなどで質問してください

 

それではまた