Yaminabe

らくがきぶろぐ

Yaminabe:5/637
てすと:Yaminabeちゃんは生まれたばかりのブログです みんなで仲よく使ってね




以前にGrabPass命令の解説をしましたが、それに関する補足説明とそれに関連してシェーダのUnityでのスクリーンスペース系の演算について補助的に解説をします。

 

Grab命令自体はイメージエフェクト処理の実行速度が解像度に比例して高コストになるため これを回避する方法として出来る限りフォワードベースに計算を移し替えてしまう方法で処理負荷の軽減を図るという意味では非常に有効な機能です 特に非力なモバイル系などで効果が期待できるのですが

ところが公式に実装された機能であるGrabPass命令が、それほど実行速度が早くないという問題があります。レンダーテクスチャをシェーダにセットしたほうがGrabPass命令に比べて処理速度が早いのですね。

そこでレンダーテクスチャとコマンドバッファーを組み合わせて最適化する方法があります。

"RenderingCommandBuffers50b22” のデモ”CommandBufferBlurRefraction.cs”と"GlassWithoutGrab.shader" のサンプルがそれです 。参考にコードを記載します、 

 



”CommandBufferBlurRefraction.cs" 

using UnityEngine;
using UnityEngine.Rendering;
using System.Collections.Generic;

// See _ReadMe.txt for an overview
[ExecuteInEditMode]
public class CommandBufferBlurRefraction : MonoBehaviour
{
    public Shader m_BlurShader;
    private Material m_Material;

    private Camera m_Cam;

    // We'll want to add a command buffer on any camera that renders us,
    // so have a dictionary of them.
    private Dictionary m_Cameras = new Dictionary();

    // Remove command buffers from all cameras we added into
    private void Cleanup()
    {
        foreach (var cam in m_Cameras)
        {
            if (cam.Key)
            {
                cam.Key.RemoveCommandBuffer (CameraEvent.AfterSkybox, cam.Value);
            }
        }
        m_Cameras.Clear();
        Object.DestroyImmediate (m_Material);
    }

    public void OnEnable()
    {
        Cleanup();
    }

    public void OnDisable()
    {
        Cleanup();
    }

    // Whenever any camera will render us, add a command buffer to do the work on it
    public void OnWillRenderObject()
    {
        var act = gameObject.activeInHierarchy && enabled;
        if (!act)
        {
            Cleanup();
            return;
        }
        var cam = Camera.current;
        if (!cam)
            return;

        CommandBuffer buf = null;
        // Did we already add the command buffer on this camera? Nothing to do then.
        if (m_Cameras.ContainsKey(cam))
            return;

        if (!m_Material)
        {
            m_Material = new Material(m_BlurShader);
            m_Material.hideFlags = HideFlags.HideAndDontSave;
        }

        buf = new CommandBuffer();
        buf.name = "Grab screen and blur";
        m_Cameras[cam] = buf;

        // copy screen into temporary RT
    int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
    buf.GetTemporaryRT (screenCopyID, -1, -1, 0, FilterMode.Bilinear);
        buf.Blit (BuiltinRenderTextureType.CurrentActive, screenCopyID);

        // get two smaller RTs
        int blurredID = Shader.PropertyToID("_Temp1");
        int blurredID2 = Shader.PropertyToID("_Temp2");
        buf.GetTemporaryRT (blurredID, -2, -2, 0, FilterMode.Bilinear);
        buf.GetTemporaryRT (blurredID2, -2, -2, 0, FilterMode.Bilinear);
        // downsample screen copy into smaller RT, release screen RT
        buf.Blit (screenCopyID, blurredID);
        buf.ReleaseTemporaryRT (screenCopyID);
        // horizontal blur
        buf.SetGlobalVector("offsets", new Vector4(2.0f/Screen.width,0,0,0));
        buf.Blit (blurredID, blurredID2, m_Material);
        // vertical blur
        buf.SetGlobalVector("offsets", new Vector4(0,2.0f/Screen.height,0,0));
        buf.Blit (blurredID2, blurredID, m_Material);
        // horizontal blur
        buf.SetGlobalVector("offsets", new Vector4(4.0f/Screen.width,0,0,0));
        buf.Blit (blurredID, blurredID2, m_Material);
        // vertical blur
        buf.SetGlobalVector("offsets", new Vector4(0,4.0f/Screen.height,0,0));
        buf.Blit (blurredID2, blurredID, m_Material);

        buf.SetGlobalTexture("_GrabBlurTexture", blurredID);

        cam.AddCommandBuffer (CameraEvent.AfterSkybox, buf);
    }   
}

// copy screen into temporary RT
int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
buf.GetTemporaryRT(screenCopyID, -1, -1, 0, FilterMode.Bilinear);
buf.Blit(BuiltinRenderTextureType.CurrentActive, screenCopyID);

 

CommandBufferBlurRefraction.csのこの部分が レンダリング前にスクリーンをキャプチャし これはシェーダ側のGrabPassと同様の動作をします
    

     // copy screen into temporary RT
    int screenCopyID = Shader.PropertyToID("_ScreenCopyTexture");
    buf.GetTemporaryRT (screenCopyID, -1, -1, 0, FilterMode.Bilinear); 
    buf.Blit (BuiltinRenderTextureType.CurrentActive, screenCopyID);

次にこの行で buf.SetGlobalTexture命令を使用してglobalテクスチャに変換しています。これによりシーン中のどのシェーダからでもセットされたテクスチャを参照することができるようになります。 この場合ははブラー処理をしたテクスチャをセットしているので次のように

  •       buf.SetGlobalTexture("_GrabBlurTexture", blurredID);

このような行を加えればレンダリングの前のスクリーン画像のキャプチャがテクスチャとしてコピーされることになります。

  •       buf.SetGlobalTexture("_ScreenCopyTexture",screenCopyID);

 

シェーダ側には以下の部分を書いておけばスクリーンキャプチャされたテクスチャ参照ができます

"vertex shader"

       o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
       o.uvgrab.zw = o.vertex.zw;

 

”fragment shader”
      sampler2D _GrabBlurTexture;
      ..........
      half4 col = tex2Dproj (_GrabBlurTexture, UNITY_PROJ_COORD(i.uvgrab));

 

すでにGrab命令を使用して作成したシェーダを修正する場合は"_ScreenCopyTexture"のテクスチャ名を”_GrabTexture”に変更してGlobal指定し

      GrabPass { "_GrabTexture" }

シェーダから GrabPass { "_GrabTexture" } の部分を削除します シェーダで描画前にスクリーンキャプチャを行う指定部分なのでこれは必要がありません。あとはgrabPassに使用した命令はそのままで置き換えが完了します。

あとはマテリアル単位でGrabTexture とBlurTextureをlerp()命令で個々にコントロールすればブラーのかかり具合が変わります。

     
   fixed3 RefCapture  = tex2D(_GrabTexture ,screenuv).xyz;
      fixed3 RefCapture1= tex2D(_GrabBlurTexture ,screenuv).xyz;
      RefCapture = lerp( (RefCapture , RefCapture1,1.0 - _AmountReflectionBlur);
      returnColor = lerp( (RefCapture , returnColor,  _refFactor);

次にシェーダの最適化を行います



■シェーダ最適化のTips   

■ _MainTexのフェッチ(読み込み)はなるべくシェーダの先頭に記述すること。_MainTexは大部分のシェーダで記述されていますが,シェーダの計算時に頻繁にアクセスされると実行速度の低下を引き起こすため シェーダ内で多くの計算を行う前にデータフェッチを完了しておきます。

■ _GrabTextureのフェッチはシェーダのなるべく最後の方に記述します。シェーダがGrabPass命令を受けてからtextureをレンダリングしてGPUにセットする動作を完了するまでなるべく時間をかせいでおくこと。GrabTextureのフェッチ前にGrabTextureのレンダリングが修了していない場合シェーダは待機状態になってしまい処理速度が低下します。

 

最適化前:

half4 frag( v2f i ) : COLOR
{
   // calculate perturbed coordinates
    half2 bump = UnpackNormal(tex2D( _BumpMap, i.uvbump )).rg;

    float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy;
    i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;

   half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
 
    half4 tint = tex2D( _MainTex, i.uvmain );
    return col * tint;
}

最適化後:

half4 frag( v2f i ) : COLOR
{

     packedNormals = tex2D( _BumpMap, i.uvbump );

     half4 tint = tex2D( _MainTex, i.uvmain );

//Grabテクスチャからサンプリングを行う前に, 多くの計算式を記述してしまうこと
// ハードウェアが main texを参照するために多くの時間を浪費する, そのためGrabTextureをサンプリングする前になるべく 多
くの空き時間を 作っておく

     half2 bump = UnpackNormal(packedNormals).rg;
     float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy;
     i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy; 
     half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
     return col * tint;
}

例の最適化前の例ではMainTextureとGrabTextureの位置が逆に記述されています。 このようにフォワードシェーダではレンダリングテクスチャを使用する場合に少し調整をする必要があります。

 

■補足解説

テクスチャサンプラーの参照は通常のシェーダ関数の計算に比べて100倍程度重たい処理となるので(クロック値ですね)なるべくテクスチャ参照回数を減らすことが演算速度の向上につながります。ブラーシェーダを例に上げると5×5のカーネルのフィルターをそのまま1パスで実装すると1ピクセルあたり25回のテクスチャ参照が行われることになりますが、 縦方向と横方向(VとH = V,U)の2パスに分離することで縦方向に5回+横方向に5回の10回のテクスチャ参照に抑えることができるためシェーダの処理速度は向上します。 レンダリングパスは増加しますがテクスチャ参照回数を減らすほうがメリットが有る場合があるわけです.

GPU演算はCPUに比較すると4,50倍程度演算能力は高いので軽量なシェーダを回す分には差が感じられないかもしれませんが スクリーン全体のエフェクトなどのように描画ピクセル数が増えると最適化の恩恵が出てくるわけです。画面表現がシンプルなゲームではそれほど最適化に気をつかう必要はないと思います。

GAUSSIANjpg

以上のような手法で画面全体をレンダリングするイメージエフェクトはなるべくフォワードベースのシェーダに変換してしまえば、非力なGPUハードウェアでも ある程度負荷の高いエフェクト処理をゲーム制作に活用することができます。もちろんディファードレンダリングをメインに使用する場合でもディファードレンダリングの苦手な処理を別カメラでフォワードレンダリングでレイヤー合成すれば 描画負荷は軽減できます。

 

 

■ここからスクリーンスペース系のシェーダに関する解説を追加しておきます

すでにご存知の情報もあると思いますが、これから学習する人もいるので

おおまかにスクリーンスペース系のシェーダの流れはものにもよりますが以下のようになります

  1. スクリプト内:カメラ行列の再計算→シェーダにセット
  2. depth、normalテクスチャからワールド座標など必要なデータをを再計算
  3. レンダリング後のテクスチャをブラーパスでぼかす
  4. 背景キャプチャしたテクスチャをレイヤ合成する

 

以前の記事でもシェーダの行列について解説はしているのですがもう一度おさらいをします。

 

  • オブジェクト座標 = DCCツールなどでモデルがセーブされた座標に存在する状態で初期座標 多くの場合は原点を中心
  • ワールド座標 = シーン中の空間に配置された状態のモデル座標 (移動、 回転、 スケール)をかけた状態
  • ビュー座標 = カメラ正面にモデルを配置された状態の座標系 正投影でパースがかかっていない
  • プロジェクション座標 = カメラからの震度に応じてパースがかかった座標系 簡単に表すとデプス値で座標を割り算する

これら座標系の変換のためにスクリプトおよびシェーダ内には主にM、V、P という基本の行列が存在しています

  • M :モデルワールド変換行列 =オブジェクト座標→ワールド座標へ
  • V : カメラビュー変換行列 = ワールド座標→ビュー座標へ
  • P:カメラプロジェクション変換行列 = ビュー座標→プロジェクション座標へ

通常の場合 頂点シェーダにのはじめに UNITY_MATRIX_MVPというマクロ命令を頂点データに掛けますがその中身が M*V*Pでモデルをオブジェクト座標からプロジェクション座標に一度に変換をしているということになります、行列というのはそれぞれを掛けることで一度に変換する合成の行列を作成することができます。

※ Unity5.x以降ではUNITY_MATRIX_MVPに代わって UnityObjectToClipPos( ) の使用を推奨されるようになりました ClipPosはスクリーン上プロジェクション座標です。 CGincludeの記述を見てみるとmul(UNITY_MATRIX_MVP, float4(pos, 1.0))mul(UNITY_MATRIX_VP, float4(pos, 1.0))をif分岐されています。

  •   float4 UnityObjectToClipPos( in float3 pos ) or float3 UnityObjectToViewPos( in float3 pos )

Unityの過去のバージョンで頂点シェーダの演算にはUNITY_MATRIX_MVP を使用していましたがスタティックモデルはワールド座標に配置済みのメッシュモデルなので モデル座標からワールド座標への変換マトリクスを掛ける必要が無いためUNITY_MATRIX_VP を掛ければ同様の座標変換が行われます。1頂点あたりMATRIX4×4=積計算が16回分節約することで処理速度が向上できるということになります。

例えばシェーダを記述するときにワールド絶対座標に配置されたメッシュモデル(メッシュ結合されてワールドに配置済みの背景、パーティクルなど)M:モデルワールド変換行列を掛ける必要がないので、mul((頂点データ),MVP)mul((頂点データ),VP) で置き換えてしまえばメッシュモデル1頂点につき4×4行列の掛け算=16回の演算が省略できるためシェーダの軽量化につながります。経験的には商業ゲームのシェーダだとおおまかに背景用とキャラクタ用に分けていることが多いので手動で切り分けられなくもないんですけども。

M :モデルワールド変換を行う行列はUnity内に_Object2World , _World2Object の2種類のマクロ定義された行列がありCGINCファイル内に記述されています。

  • _Object2World (ver5以降:unity_ObjectToWorld) = オブジェクト空間からワールド空間の座標へ変換。
  • _World2Object (ver5以降:unity_WorldToObject) = ワールド空間からオブジェクト座標への変換。

_Object2World , _World2Object の行列はそれぞれが逆の変換をおこないますがこれを逆行列変換と呼びます。 _World2Object

_Object2World 逆行列となります

V:カメラビュー座標がわかりにくいかもしれませんが カメラの正面にモデルが配置されたノンパース状態 ビューベクトルは{カメラの座標 ? モデル座標 }で計算をしますがスクリーン上でどのピクセルにおいてもカメラに向かうビューベクトルはfloat3(0,0,1)ということになります。 と書くとわかりやすいですか? きちんと意味を理解をしておけば処理速度を稼ぎたい場合判明している値はこうした定数で置き換えることで最適化に役立ちます。

ここからはイメージエフェクトなどのスクリーンスペース計算に該当するシェーダを参考にしてもらいたいのですが、主にスクリーンスペース系のシェーダではすでに変換されたシーン画像データからの逆変換を行って必要な値を取得するところが通常のシェーダ処理と異なっています。

UnityシェーダではNormalとDepth の2種類のテクスチャがオプションでベイクすることが出来ますが基本的にはこれらのテクスチャを逆行列で変換することで計算に必要なワールド座標などを取得する動作が基本となります。もちろん計算が複雑な場合など必要であればGバッファなどを用いてカスタムテクスチャをベイクしてもかまわないです 何度も同じ計算をする場合など、たとえばワールド座標をRGB値に変換してテクスチャ化してしまえば処理速度は向上します。



■カメラ行列の再計算

先程述べたようにモデル←→ワールド座標変換については_Object2World , _World2Object が存在しますが、VおよびPの逆変換行列がシェーダ内では記述がありませんのでスクリプト側でVおよびPの逆行列を作成してシェーダにセットする必要があります。

  • version5.0以前古い形式では以下のように記述していました。こちらは行列の操作がわかりやすいので参考までに

void Update(){

bool d3d = SystemInfo.graphicsDeviceVersion.IndexOf("Direct3D") > -1;
Matrix4x4 M = transform.localToWorldMatrix;
Matrix4x4 V = camera.worldToCameraMatrix;
Matrix4x4 P = camera.projectionMatrix;

if (d3d) {
// テクスチャのY座標(UV 値のV値)を反転する
    for (int i = 0; i < 4; i++) {
        p[1,i] = -p[1,i];
    }
// スケール、バイアスを OpenGLから D3D 深度に変換する
    for (int i = 0; i < 4; i++) {
        p[2,i] = p[2,i]*0.5f + p[3,i]*0.5f;
    }
}

float4x4 MVP = P*V*M;  // OpenGLとD3Dでは行列を掛け合わせる方向が逆になります

Shader.SetGlobalMatrix("_matMVPI", MVP);
Shader.SetGlobalMatrix("_matMVPI", MVP.inverse);
}

float4x4 ModelViewProjI = _matMVPI;

}

【Script側】(現在はこのように記述してください  必要なものだけでいいです)

void Update()
{

_worldToCameraMatrix = _camera.worldToCameraMatrix; //ワールドからカメラ座標へ変換する行列
_cameraToWorldMatrix = worldToCameraMatrix.inverse; //
_worldToCameraMatrixの逆行列
_projectionMatrix           = GL.GetGPUProjectionMatrix(_camera.projectionMatrix, false); //プロジェクション行列
_viewProjectionMatrix   = projectionMatrix * worldToCameraMatrix; //プロジェクションをカメラビューに変換する
_projectionMatrix の逆
_inverseViewProjectionMatrix = viewProjectionMatrix.inverse; // VPの逆行列つまりM(モデルからワールドへ変換行列)
_worldToLocalMatrix     = this.transform.worldToLocalMatrix; //ワールド座標をローカル座標へ変換する行列

Shader.SetGlobalMatrix("_ProjectionMatrix", projectionMatrix);
Shader.SetGlobalMatrix("_ViewProjectionMatrix", viewProjectionMatrix);
Shader.SetGlobalMatrix("_InverseProjectionMatrix", projectionMatrix.inverse);
Shader.SetGlobalMatrix("_InverseViewProjectionMatrix", inverseViewProjectionMatrix);
Shader.SetGlobalMatrix("_WorldToCameraMatrix", worldToCameraMatrix);
Shader.SetGlobalMatrix("_CameraToWorldMatrix", cameraToWorldMatrix);
Shader.SetGlobalMatrix("_CameraWorldToLocalMatrix", worldToLocalMatrix);

}

  • _worldToCameraMatrix =                  ワールドからカメラ座標へ変換する行列
  • _cameraToWorldMatrix =                  _worldToCameraMatrix.の逆行列
  • _projectionMatrix           =                  プロジェクション行列
  • _viewProjectionMatrix   =                  プロジェクションをカメラビューに変換する_projectionMatrixの逆逆行列
  • _inverseViewProjectionMatrix =       VPの逆行列つまりM(モデルからワールドへ変換行列
  • _worldToLocalMatrix     =                   ワールド座標をローカル座標へ変換する行列

Shader.SetGlobalMatrix("シェーダ側で宣言する変数名" ,セットする要素(ここでは行列)) シーンの中のスクリプトでセットします。

【Shader側】シェーダ側でグローバルパラメータを取得するためのマトリクスを宣言します。

uniform float4x4  _ProjectionMatrix;
uniform float4x4  _ViewProjectionMatrix;
uniform float4x4  _InverseProjectionMatrix;
uniform float4x4  _InverseViewProjectionMatrix;
uniform float4x4  _WorldToCameraMatrix;
uniform float4x4  _CameraToWorldMatrix;
uniform float4x4  _CameraWorldToLocalMatrix;

※uniform :コード内のどこからでも呼び出せる型宣言

_ProjectionMatrix などはすでにUnityのシェーダ内で用意されている行列Pが存在していますが再計算が必要となります。Unityの仕様上の問題でスクリプト側ではGL系を採用していますが、シェーダ側ではDirectXまたはGL系といった複数の形式に対応しているため値のズレが出てしまうのです。これは Unityが汎用エンジンを目指して複数形式に対応した結果のひずみと考えられそうです。

■参考までに逆行列をカスタムで記述する場合は以下のようになります。あまりないですが自前で記述するときに

float4x4 inverse(float4x4 input)

{

#define minor(a,b,c) determinant(float3x3(input.a, input.b, input.c))
  //determinant(float3x3(input._22_23_23, input._32_33_34, input._42_43_44))

    float4x4 cofactors = float4x4(
    minor(_22_23_24, _32_33_34, _42_43_44),
    -minor(_21_23_24, _31_33_34, _41_43_44),
    minor(_21_22_24, _31_32_34, _41_42_44),
    -minor(_21_22_23, _31_32_33, _41_42_43),

    -minor(_12_13_14, _32_33_34, _42_43_44),
    minor(_11_13_14, _31_33_34, _41_43_44),
    -minor(_11_12_14, _31_32_34, _41_42_44),
    minor(_11_12_13, _31_32_33, _41_42_43),

    minor(_12_13_14, _22_23_24, _42_43_44),
    -minor(_11_13_14, _21_23_24, _41_43_44),
    minor(_11_12_14, _21_22_24, _41_42_44),
    -minor(_11_12_13, _21_22_23, _41_42_43),

    -minor(_12_13_14, _22_23_24, _32_33_34),
    minor(_11_13_14, _21_23_24, _31_33_34),
    -minor(_11_12_14, _21_22_24, _31_32_34),
    minor(_11_12_13, _21_22_23, _31_32_33)
    );
#undef minor
    return transpose(cofactors) / determinant(input);
}

 

4x4Mat



■GL系とDirectX系の差異の解消

 GL系とDirectX系でのシェーダ内で記述する際のポイントをあげてみます

スクリーンスペース上でのベクトルはGLとDirectXはUVのV値が上下反転されているので DirectXの場合 uvはy=1-uv.y ベクトルには_ProjectionParams.を掛けることで正確な値となります。

  • Direct3Dでは、座標は上部がゼロで下向きに増大します。
  • OpenGLOpenGL ESでは、座標の下部がゼロで上向きに増大します。

■GLとDirectXのテクスチャ座標が上下反転の解消

#if defined(SHADER_API_D3D9) || defined(SHADER_API_D3D11)    
           uv.y = 1 ? uv.y;
#endif

ベクトルの上下反転の解消

#if defined(SHADER_API_D3D9) || defined(SHADER_API_D3D11)            
            vector.y *= _ProjectionParams.x;
#endif

_ProjectionParams はそれぞれ

  • x:プロジェクションマトリクスのフリップ情報 1.0(GL系) 、-1.0(DirectXなど)
  • y:cameraのnear plane
  • z:cameraのfar plane
  • w:1/FarPlane

■HLSL とGLでは行列の扱いが異なります

  • HLSL系では以下のように要素が並びますが

   AAAA                            
   BBBB
   CCCC
   DDDD

  • GL系では縦と横の要素が入れ替わっています。これを 転置=transpose(転置行列) と呼びます。

   ABCD
   ABCD
   ABCD
   ABCD

■HLSL とGLでは要素をかける順番が変わってきます

  • HLSL: mul(m,n) GLSL: n*m
  • HLSL: Matrix4x4 MV= mul(M,V); Matrix4x4 MVP = mul(MV,P);
  • GL:    Matrix4x4 MVP = P*V*M;

C#Unity側:マトリクス行列を取得するときに”.inverse”、”.transpose” を指定すると逆行列、転置行列を取得できます

  • Matrix4x4.inverse
  • Matrix4x4.transpose

Shader側:GL、HLSLに inverse(float4x4 m)、transpose(float4x4 m) という関数が存在します。

  • float4x4 inverse(float4x4 m)
  • float4x4 transpose(float4x4 m)
  • mat4 inverse(mat4 m);
  • mat4 transpose(mat4 m);

 



GL - HLSLの変換 ( ※GL - HLSL変換のスクリプトというのもあります)

スクリーンスペース系のシェーダを書く際 あるいは練習用に Direct3Dに移植する場合GL系からHLSL系にまたはその逆でシェーダーをリライトする機会があると思いますのでGL - HLSL変換の簡単なまとめを記述してみます。

この情報元では ShaderToy https://www.shadertoy.com/ でシェーダ記述の練習をするために書かれた記載です。

sahderToy

  • iGlobalTime※GL - HLSL変換のスクリプトもありますシェーダ入力(シェーダ再生時間(秒))は_Time.yを使用します。
  • iResolution.xy(「ピクセル単位のビューポート解像度」)_ScreenParams.xyを使用します。
  • float2をvec2タイプへ、float2x2をmat2へ
  • vec3(1,1,1)すべての要素が明示的なfloat3(1,1,1)で同じ値を持つショートカットコンストラクタ
  • Texture2D をTex2D
  • atan(x、y) を atan2(y、x)<パラメータの順序に注意してください!
  • mix()をlerp()
  • * = を mul()
  • Texture2Dルックアップから第3パラメータ(バイアス)を削除
  • mainImage(out vec4 fragColor、vec2 fragCoord)は、フラグメントシェーダ関数です。 float4 mainImage(float2 fragCoord:SV_POSITION):SV_Target
  • GLSLのUV座標は、上端が0で下向きになり、HLSL 0は下端にあり増加します uv.y = 1 - uv.yを使用する必要があります。

 



■DepthNormalテクスチャを使用した再計算

基本のDepth値と頂点の法線(ノーマル)値はスクリプト側でテクスチャレンダリングモードを設定できます

  • DepthTextureMode.Depth: depth値のみベイクする場合はこのモードを選択します
  • DepthTextureMode.DepthNormals: depth値+ノーマル値をテクスチャにパックするモードの場合はこちらを選択します

スクリプト側:スクリプト内のどこかでセットアップ時にテクスチャモードを設定してください

void OnEnable() {

camera.depthTextureMode = DepthTextureMode.Depth;

camera.depthTextureMode = DepthTextureMode.DepthNormals;

}

【シェーダ側】

      uniform sampler2D _CameraDepthNormalsTexture;

//depthテクスチャはグローバルタイプなのでプロパティ指定は不要

      float4 encodedDepthNormal = tex2D(_CameraDepthNormalsTexture, screenUV.xy);
      float depth;
      float3 normal_vs;
      DecodeDepthNormal(encodedDepthNormal, depth, normal_vs);

32ビットチャンネルのテクスチャの内訳は R+Gチャンネルにビュー空間法線がエンコードされ、B+Aチャンネル(16ビット)にdepth値がエンコードされています depth=DecodeFloatRG(_CameraDepthNormalsTexture.zw )depthだけをとれることになります 。 テクスチャのアンパックにはUnityのヘルパ関数 DecodeDepthNormal()を使用します。


DecodeDepthNormal()関数で返されるdepth値は0~1のFloat型です これはカメラ座標からfrustum(far)の距離に相当します。

depth値だけ参照したい場合は次のように

      geom = tex2Dlod (_CameraDepthNormalsTexture, float4(screenUV.xy,0,0));
      sampleDepth  = DecodeFloatRG(geom.zw);
      currentDepth  = Linear01Depth(samplePos.z);

Linear01Depth(samplePos.z) : depthを0から1の値に変換することでデータが扱いやすくなります。普通に掛けるときも便利ですが、たとえばカメラからの距離で値を割る様な場合に直接値で割ると値が1.0以上なら普通に割れますが1.0以下の値で割ると値は0に向かって増大するため1以上1未満で場合分けが必要になります

※DX11使用時にはデフォルトではDepthテクスチャを作成してくれませんので ”ZWrite On” をシェーダに記述します



■スクリーンスペースからワールド座標の再計算

float3 GetScreenPos (float2 uv, float depth)
{
    return float3(uv.x * 2 - 1, uv.y * 2 - 1, depth);
}

float3 GetWorldPos (float3 screenPos)
{
    float4 worldPos = mul(_InverseViewProjectionMatrix, float4(screenPos, 1));
    return worldPos.xyz / worldPos.w;
}

float3 GetViewPos (float3 screenPos)
{
    float4 viewPos = mul(_InverseProjectionMatrix, float4(screenPos, 1));
    return viewPos.xyz / viewPos.w;
}
   
float3 GetViewDir (float3 worldPos)
{
    return normalize(worldPos - _WorldSpaceCameraPos);
}

 

GetScreenPos (float2 uv, float depth) :スクリーンUVとdepthを与えてスクリーン座標が出れば
それをカメラの逆行列にかければワールド座標とビュー座標のポジションが確定します。 後必要なのはビューベクトルで

normalize(worldPos - _WorldSpaceCameraPos)でビュー正規化ベクトルが取得できますので、これにdepthをかけるとワールドのポシションが参照できます。

worldPos.xyz / worldPos.w; のwは何かというとプロジェクション行列の場合は

カメラ奥行きに対して頂点座標ごとのフラスタム(画角ですね)のサイズ比率が帰ってくるのでwで割ることで補正を解除するということです。

モデル頂点の場合はwには全体スケールなんかが入っている場合がありますね 普通は使用しないリザーブの値なので何が登録されているかはケースによります。

■UnityのデモのDeferdDecalの unity_CameraToWorld, unity_WorldToObject というヘルパー関数でもカメラワールド変換できるみたいなので自分で書かなくてもいいのかもしれないです。

// read depth and reconstruct world position
                float depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
                depth = Linear01Depth (depth);
                float4 vpos = float4(i.ray * depth,1);
                float3 wpos = mul (unity_CameraToWorld, vpos).xyz;
                float3 opos = mul (unity_WorldToObject, float4(wpos,1)).xyz;

■ほかにもワールド座標を取得する場合 以下はUnity5デモのサンプルコードRenderingCommandBuffers50b22”  から抜粋した部分で こんな計算方法もあるらしいんですが 検証してません

 

CBUFFER_START(UnityPerCamera2)
float4x4 _CameraToWorld;          //<- カメラ2ワールド変換はコレを参照してもいいらしい
CBUFFER_END

  sampler2D _MainTex; 
  sampler2D _DecalTex;

  sampler2D_float _CameraDepthTexture;


            float3 wpos = mul (_CameraToWorld, vpos).xyz;

                       i.ray = i.ray * (_ProjectionParams.z / i.ray.z);
            float2 uv = i.screenUV.xy / i.screenUV.w;
           // read depth and reconstruct world position
            float  depth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, uv);
                      depth = Linear01Depth (depth);
            float4 vpos = float4(i.ray * depth,1);
            float3 wpos = mul (_CameraToWorld, vpos).xyz;
            float3 opos = mul (_World2Object, float4(wpos,1)).xyz;

            clip (float3(0.5,0.5,0.5) - abs(opos.xyz));

※Unity4.6から sampler2D_float _CameraDepthTexture; のように サンプラーに_floatをオプションで記述することで テクスチャサンプラーで強制的に型を指定することができるようになりました 高い精度が必要な場合は指定してください。

 



■Depth値から法線の再計算

法線情報はdepthnormalテクスチャから取得できるので必要では無いのですけど 追加情報ということで書いておきます。

GLコードのままでもうしわけないですが。 プロシージャルなイメージなどの場合に法線の再計算が必要な場合が出てくるので そうした用途です・

a:

//w = linear depth
vec3 getViewSpacePosition(in float w)


vec4 pos = vec4((gl_TexCoord[0].st - depth_size.xy * 0.5) / (depth_size * 0.5), vec2(1.0))
                * w * gl_ProjectionMatrixInverse;
        pos.y *= -pos.z * (far - near) / near;
        pos.x *= pos.z * (far - near) / near;
    return pos.xyz;
}
vec3 normal = normalize(cross(ddx(pos.xyz),ddy(pos.xyz)));

b:

vec4 pos = vec4((gl_TexCoord[0].st - depth_size.xy * 0.5) / (depth_size* 0.5), w, 1.0) * w * gl_ProjectionMatrixInverse; 
vec3 n = normalize(cross(dFdx(pos.xyz), dFdy(pos.xyz))) * 0.5 + 0.5;

c:
vec3 normal = normalize(vec3(dFdx(w) * 500.0, dFdy(w) * 500.0, 1.0)) * 0.5 + 0.5;

 



TANGENT_SPACE_ROTATIONを使用したベクトル計算

grabテクスチャを使用したフォワードベースのシェーダでスクリーンスペース計算を行うときはTANGENT_SPACE_ROTATIONの使用が適しています。

AssetStoreの『HARDSURFACE SHADER』『Candela SSRR 』などがこの方法でシェーダー実装されていると公式フォーラムでアドバイスがされていたのですが 現在では前者はPROバージョンのサポートが終了してしまい現在はコード難読化、 後者はデフォルトで難読化されていて確認が困難なため、実装について簡単に解説をしておきます。

 

tangentRot

struct v2f {
                float4 pos : SV_POSITION;
                float2    uv : TEXCOORD0;
                float3 TtoV0 : TEXCOORD1; 
                float3 TtoV1 : TEXCOORD2;
            };

TANGENT_SPACE_ROTATION;
                    o.viewDir.xyz = mul( rotation, ObjSpaceViewDir( v.vertex ) );
                    o.TtoV0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz); 
                    o.TtoV1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);

 

これにはUnify Community WikiのMatCap シェーダで使用されている方法を応用します。いちおう解説を

TANGENT_SPACE_ROTATION マクロを指定して2D座標系(スクリーン上のタンジェントスペース)の回転ベクトルに変換するための行列を作成しています。タンジェントスペースというのは法線に対して直行するテクスチャ上のUV座標系のことです 要は通常は法線からテクスチャのUVを求める関数をスクリーン上のUV計算に応用しているわけです。

■スクリーンスペース系(ビュー座標)でベクトルの扱い

例としてビュー座標系のベクトル計算のサンプル一覧をマニュアルから抜粋します。

Reflections

MatCapシェーダの例では UNITY_MATRIX_IT_MVを使用しています この場合のITは逆転置行列(InverseTranspose)と呼ばれベクトルをビュー座標変換するために使用します、Unity内にはワールド変換する行列は_Object2Worldもあるのですが,_Object2Worldは座標変換する場合に使用し UNITY_MATRIX_IT_MVはベクトルの変換に使用して欲しいということです。

UNITY_MATRIX_IT_MV“UnityCG.cginc” 内では次のような記述がされています。

// Declares 3x3 matrix 'rotation', filled with tangent space basis
#define TANGENT_SPACE_ROTATION
    float3 binormal = cross( normalize(v.normal), normalize(v.tangent.xyz) ) * v.tangent.w;
    float3x3 rotation = float3x3( v.tangent.xyz, binormal, v.normal )

これはシェーダにTANGENT_SPACE_ROTATIONのマクロを記述した場合 float3x3 rotation 行列に(タンジェント成分、複法線、法線)が帰ることを意味しています。法線normal(ここではスクリーンに対してZ方向成分)は使用しないのでスクリーン上のXY座標(UV)に当たるtangent, binormalだけを取り出して使用します。その部分が次の行列計算で

  o.TtoV0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz);
  o.TtoV1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz);

のように rotation 行列とUNITY_MATRIX_IT_MV行列2つを掛けて、オブジェクト座標からスクリーンxy座標に直接変換できるように行列TtoV0、TtoV1を定義します。これによってオブジェクト空間のベクトルはTtoV0、TtoV1を使用してビュー空間のスクリーン座標上で2次元のUVベクトルに変換することが出来ます。

 

■ここからはフラグメントシェーダの解説です SSR(スクリーンスペースリフレクションの計算をイメージしています)

  • ここからは AssetStoreで配布されている『HARDSURFACE SHADER』 の定義を参考に書きます。(現在のバージョンではコードが難読化されていますので以前配布されていたフリーのものです)

次に得られたベクトルを目的に合わせて加工する必要があります。法線 (ここではバンプマップの法線)をそれぞれの行列をかけてビュー座標系のUVに変換します

      half2 vn; //vn:ビューノーマル 
        vn.x = dot(IN.TtoV0, Bumpnormal); 
        vn.y = dot(IN.TtoV1, Bumpnormal);

 

vec0

    
       half2  absvn = abs(vn); //ビューノーマルを絶対値に変換  
        half   maxvn = max(absvn.x,absvn.y); //ベクトル長さの大きな方を選択  

                  maxvn = 1 / maxvn; //ベクトルの長さで割り算  
                  vn = vn * maxvn;

  アスペクト比を吸収しますUV長さ(1,1)はゲーム画面で通常横長楕円になるのでベクトルの長い成分の絶対値で割ります

vec1

ベクトルの最大長さは1で帰りますがそのまま計算に使用するとスクリーンから溢れてしまうためスクリーン座標の縦横のサイズの大きな方で割ることでベクトルの長さをピクセルサイズ以下の大きさに調整します。イメージなので実際はもっと小さい円です。

vec2

ベクトルのy方向を使用するシェーダ種類(GLかDirectX)によってY方向の反転処理をします。_ProjectionParams.xに 1または−1で帰ってきます。

      #if defined(SHADER_API_D3D9) || defined(SHADER_API_D3D11)
               vn.y *= _ProjectionParams.x;
      #endif
     //uniform float4 _ProjectionParams; 
     // x = 1 or -1 (-1 if projection is flipped)
     // y = near plane 
     // z = far plane  
     // w = 1/far plane

 

■ディファードレンダリングまたはイメージエフェクトなどのスクリーン全体をレンダリングする処理の場合はこのあとにレイマーチング処理が入ります。レイマーチングをレイトレーシングと取り違える人がいますが、レイマーチング処理はベクトルのスライス処理でループ内でステップ数分の演算を行うテクニックですがレイトレーシングとは別の手法です。

この部分はdepth値を使用して背景との重ね合わせ判定を行うための処理なのですでにソートが済んでいるフォワードシェーディングの場合は必要なくなります。重たいループ部分が無い分シェーダが軽量化されるというわけです。

レイマーチング法はすでに他のサイトでの解説があると思いますので簡単に書きます。


例えば反射ベクトルを例にすると ベクトルからワールド座標が算出できるとカメラからの深度が計算できます depthテクスチャの深度とカメラからの深度をを比較してレイベクトルがdepthテクスチャの深度より奥にあればレイベクトルの追跡を打ち切る(discard)という動作です。

int Nsamples= 120; //大体100回程度のサンプリング数
float offset=0;
//  オフセット値が先程のスライスされたベクトル vn にあたります

for( i=0; i
         float  stepDepth = スクリーン上のUV+offset座標からカメラからの深度を計算;
         float  decodedDepth = Linear01Depth( tex2D( _CameraDepthTexture, uv).r); //depthテクスチャを参照

          if(decodeDepth < stepDepth) discard; //デプス値に比較によってブレーク(シェーダを抜ける)です。
          else{
               sampleColor = sampleColor+ サンプリングしたカラー ;
  
         }
         offset = offset+スライスされたベクトルの増加分;
         uv =uv+offset



次にGrabTextureで背景のカラー成分を取得しますがこの時にカラーのブレンド強度を設定するためのフォールオフ(減衰)値を計算します。

     fixed2 screenedges =  abs(screenuv * 2 - 1);             

                 screenedges =  1 - (screenedges * screenedges);

  //   abs (-1< uv < 1) 絶対値をとって 0~1(画面中央が0~画面隅が1)の値を取るように変換して 2乗で減衰する

出力カラーは最終的にフォールオフ値を掛けて調整しますが、スクリーンスペース系での計算は正確なものではないため以下のような条件を回避するために減衰を設定して見栄えを良くする必要があります。

  1. 画面の端 UV値が0~1範囲の外からカラーを取得するとスクリーンをレンダリングしたテクスチャの端はロールアップされて引き伸ばされているため スクリーン画面端に向かってカラーを減衰する。     
  2. カメラビューに対して奥行き方向に歪みを軽減するためにDepth値に応じて取得するカラーを減衰する。                                                                                                                                       
  3. 法線がカメラ方向に向かっている場合。モデル表面にモデル自身が写り込んでしまうためカメラビューと法線の内積を計算してカラーを減衰させる。   手前方向など目立つところは通常の反射キューブマップ  で補完                                                                                      
  4. カラー取得の座標距離が離れるほど画像が伸びてしまうので距離に応じてカラーを減衰させる。
      fixed3 RefCapture  = tex2D(_GrabTexture ,screenuv).xyz;
      fixed3 RefCapture1= tex2D(_GrabBlurTexture ,screenuv).xyz;
      RefCapture = lerp( RefCapture, RefCapture1,1.0 - _ReflectionBlur);

背景とのカラー合成はマスクテクスチャを用意してもいいですが スペキュラマスクで代用すれば見栄えは問題ないかと思います。

この場合スペキュラ値に応じてlerp()関数で背景とのブレンドをすればよいです。

 

※カメライメージエフェクトまたはディファードレンダリングでこの方法を使用する場合はNormalテクスチャを以下のようなシェーダ内容でレンダリングします。『Candela SSRR 』のシェーダの難読化されていない部分でのレンダリングの前準備はこのようになっている様です。内容は 『HARDSURFACE SHADER』とほぼ同じ記述です

このディファード計算の場合は通常の方法でにワールド座標を計算してテクスチャ化しても速度差はあまり変わらないと思います

struct v2f
      {
        float4 pos : SV_POSITION;
        float2 uv  : TEXCOORD0;
        float3 TtoW0 : TEXCOORD1;
        float3 TtoW1 : TEXCOORD2;
        float3 TtoW2 : TEXCOORD3; 
         };
      v2f vert (appdata_tan v)
      {

        v2f o; 
         o.pos = mul (UNITY_MATRIX_MVP, v.vertex);
         o.uv = TRANSFORM_TEX (v.texcoord, _BumpMap); 
TANGENT_SPACE_ROTATION;
         o.TtoW0 = mul(rotation, UNITY_MATRIX_IT_MV[0].xyz * 1.0); 
         o.TtoW1 = mul(rotation, UNITY_MATRIX_IT_MV[1].xyz * 1.0); 
         o.TtoW2 = mul(rotation, UNITY_MATRIX_IT_MV[2].xyz * 1.0);

        return o;
      }
      fixed4 frag (v2f i) : COLOR0
      {
        fixed3 bumpNormal = UnpackNormal(tex2D(_BumpMap, i.uv));
        fixed3
viewNormal;
                    viewNormal.x = dot(i.TtoW0,
bumpNormal ); //  タンジェント (スクリーン座標U方向成分)   
                    viewNormal.y = dot(i.TtoW1, bumpNormal ); // 複法線 (スクリーン座標V方向成分)
                    viewNormal.z = dot(i.TtoW2, bumpNormal ); // 法線 

        fixed4 color; 
        color.xyz = viewNormal* 0.5 + 0.5; // ベクトル(−1,1)の範囲から (0,1)の範囲に修正ベクトルの中心を(0.5,0.5)

// color.a が空いているのでspecuar値などを追加してもよい
        return color;
      }

 

ブラーパス及びテクスチャのレイヤー合成に関しては通常のシェーダと特にかわるところはありませんので、それぞれの該当するシェーダを参考にしてください 2パスのボックスブラーはすでにコードがついてきていますので、その他の参考の実装を記載します

■フォワードシェーダでのgrabPassを使用して通常シェーダ単独でブラーエフェクトをかけるためのシェーダは以下のコードが参考になります。

“SimpleGrabPassBlur.shader”

Shader "Custom/SimpleGrabPassBlur" {
    Properties {
        _Color ("Main Color", Color) = (1,1,1,1)
        _BumpAmt  ("Distortion", Range (0,128)) = 10
        _MainTex ("Tint Color (RGB)", 2D) = "white" {}
        _BumpMap ("Normalmap", 2D) = "bump" {}
        _Size ("Size", Range(0, 20)) = 1
    }
    Category {
        // We must be transparent, so other objects are drawn before this one.
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Opaque" }
        SubShader {
            // Horizontal blur
            GrabPass {
                Tags { "LightMode" = "Always" }
            }
            Pass {
                Tags { "LightMode" = "Always" }
                CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
                struct appdata_t {
                    float4 vertex : POSITION;
                    float2 texcoord: TEXCOORD0;
                };
                struct v2f {
                    float4 vertex : POSITION;
                    float4 uvgrab : TEXCOORD0;
                };
                v2f vert (appdata_t v) {
                    v2f o;
                    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
#if UNITY_UV_STARTS_AT_TOP
                    float scale = -1.0;
#else
                    float scale = 1.0;
#endif
                    o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
                    o.uvgrab.zw = o.vertex.zw;
                    return o;
                }
                sampler2D _GrabTexture;
                float4 _GrabTexture_TexelSize;
                float _Size;
                half4 frag( v2f i ) : COLOR {
                    //                  half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
                    //                  return col;
                    half4 sum = half4(0,0,0,0);
#define GRABPIXEL(weight,kernelx) tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(float4(i.uvgrab.x + _GrabTexture_TexelSize.x * kernelx*_Size, i.uvgrab.y, i.uvgrab.z, i.uvgrab.w))) * weight
                    sum += GRABPIXEL(0.05, -4.0);
                    sum += GRABPIXEL(0.09, -3.0);
                    sum += GRABPIXEL(0.12, -2.0);
                    sum += GRABPIXEL(0.15, -1.0);
                    sum += GRABPIXEL(0.18,  0.0);
                    sum += GRABPIXEL(0.15, +1.0);
                    sum += GRABPIXEL(0.12, +2.0);
                    sum += GRABPIXEL(0.09, +3.0);
                    sum += GRABPIXEL(0.05, +4.0);
                    return sum;
                }
                ENDCG
            }
            // Vertical blur
            GrabPass {
                Tags { "LightMode" = "Always" }
            }
            Pass {
                Tags { "LightMode" = "Always" }
                CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
                struct appdata_t {
                    float4 vertex : POSITION;
                    float2 texcoord: TEXCOORD0;
                };
                struct v2f {
                    float4 vertex : POSITION;
                    float4 uvgrab : TEXCOORD0;
                };
                v2f vert (appdata_t v) {
                    v2f o;
                    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
#if UNITY_UV_STARTS_AT_TOP
                    float scale = -1.0;
#else
                    float scale = 1.0;
#endif
                    o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
                    o.uvgrab.zw = o.vertex.zw;
                    return o;
                }
                sampler2D _GrabTexture;
                float4 _GrabTexture_TexelSize;
                float _Size;
                half4 frag( v2f i ) : COLOR {
                    //                  half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
                    //                  return col;
                    half4 sum = half4(0,0,0,0);
#define GRABPIXEL(weight,kernely) tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(float4(i.uvgrab.x, i.uvgrab.y + _GrabTexture_TexelSize.y * kernely*_Size, i.uvgrab.z, i.uvgrab.w))) * weight
                    //G(X) = (1/(sqrt(2*PI*deviation*deviation))) * exp(-(x*x / (2*deviation*deviation)))
                    sum += GRABPIXEL(0.05, -4.0);
                    sum += GRABPIXEL(0.09, -3.0);
                    sum += GRABPIXEL(0.12, -2.0);
                    sum += GRABPIXEL(0.15, -1.0);
                    sum += GRABPIXEL(0.18,  0.0);
                    sum += GRABPIXEL(0.15, +1.0);
                    sum += GRABPIXEL(0.12, +2.0);
                    sum += GRABPIXEL(0.09, +3.0);
                    sum += GRABPIXEL(0.05, +4.0);
                    return sum;
                }
                ENDCG
            }
            // Distortion
            GrabPass {
                Tags { "LightMode" = "Always" }
            }
            Pass {
                Tags { "LightMode" = "Always" }
                CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma fragmentoption ARB_precision_hint_fastest
#include "UnityCG.cginc"
                struct appdata_t {
                    float4 vertex : POSITION;
                    float2 texcoord: TEXCOORD0;
                };
                struct v2f {
                    float4 vertex : POSITION;
                    float4 uvgrab : TEXCOORD0;
                    float2 uvbump : TEXCOORD1;
                    float2 uvmain : TEXCOORD2;
                };
                float _BumpAmt;
                float4 _BumpMap_ST;
                float4 _MainTex_ST;
                v2f vert (appdata_t v) {
                    v2f o;
                    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
#if UNITY_UV_STARTS_AT_TOP
                    float scale = -1.0;
#else
                    float scale = 1.0;
#endif
                    o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
                    o.uvgrab.zw = o.vertex.zw;
                    o.uvbump = TRANSFORM_TEX( v.texcoord, _BumpMap );
                    o.uvmain = TRANSFORM_TEX( v.texcoord, _MainTex );
                    return o;
                }
                fixed4 _Color;
                sampler2D _GrabTexture;
                float4 _GrabTexture_TexelSize;
                sampler2D _BumpMap;
                sampler2D _MainTex;
                half4 frag( v2f i ) : COLOR {
                    // calculate perturbed coordinates
                    half2 bump = UnpackNormal(tex2D( _BumpMap, i.uvbump )).rg; // we could optimize this by just reading the x  y without reconstructing the Z
                    float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy;
                    i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;
                    half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
                    half4 tint = tex2D( _MainTex, i.uvmain ) * _Color;
                    return col * tint;
                }
                ENDCG
            }
        }
    }
}



■LODブラーの使用について

RenderTextureの生成時にLOD生成オプションを指定しておくとtex2Dlod(sampler2D samp, float4 s)で簡易ブラー処理を代用できますが、現段階でコマンドバッファーの使用時にLOD生成のオプションは見当たらないようですので、LODを使用したいのであればコマンドバッファーに含めないBlit命令でレンダリングテクスチャを生成する必要があるようです。

GrabPassで生成されるテクスチャも同様にLODの生成が出来ず解像度の指定もできませんので(フル解像度です)ブラーテクスチャであればレンダーテクスチャで4タップ 程度の小さな解像度で生成すれば処理速度は上々だと思います。

MipMapを使用するブラーはそのまま使用すると若干ノイズがのると思いますので、ウェイトをつけて複数ポイントでサンプリングすると見た目が向上します    例えば以下のように

const int2 offsets[7] = {{-3, -3}, {-2, -2}, {-1, -1}, {0, 0}, {1, 1}, {2, 2}, {3, 3}};

const float weights[7] = {0.001, 0.028, 0.233, 0.474, 0.233, 0.028, 0.001};

    float4 mipMapBlur( VertexOutput i ) : SV_Target
    {    
        float2 uv = i.uv;

        int numSamples = 7;

        float4 result = 0.0;
        for(int i = 0; i < numSamples; i++)
        {
            float2 offset = offsets[i] * _blurDir;

            float4 sampleCol = tex2Dlod(_MainTex, float4(uv + offset, 0, _MipCount));

      result += sampleCol * weights[i];
        }

        return result;
    }

 

■それほど精度がなくても こんなのでも大丈夫だと思います。重ねて合成するので思っているほど粗が目立たないという。。

const  float  _Mip[3] = {0.0, 3.0, 5.0};

int numSamples = 3;

for(int i = 0; i < numSamples; i++) {

      float4 sampleCol = tex2Dlod(_MainTex, float4(uv, 0, _Mip[i]));

}

sampleCol /= numSamples;

 



■リフレクションマップについての補足

スクリーンスペース計算のシェーダで弱点になるひとつがカメラ方向に向かうレイベクトルで 例えば反射を扱う場合は反射マップを合成して見たをカバーする必要が出てきます。 そこら辺はすでにリリースされているSSRシェーダなどで確認してもらうとして

リフレクションキューブマップを生成する場合Unity5以前では スクリプトを使用して生成するのですがUnity5以降ではリフレクションプルーブがサポートされているので あまり必要はないでしょう。以下は公式マニュアルに掲載されているスクリプトですが C#で記述し直すとおもにIPHONE方面でバグってしまうようです。 unityのC#が完全にはマルチプラットフォーム対応しきれていない部分があり unityスクリプト(javaSCRIPT)でないと動作しない場合があるので注意してください。

カスタムなリフレクションプルーブを作成したい場合は空のオブジェクトをスクリプトをつけてシーンに配置してゲームスタート前にスクリプトを実行すればそれらの配置場所のキューブマプテクスチャがレンダリングされます。それら複数キューブマップをグローバルテクスチャ登録して反射プロパティのあるマテリアルとの距離に応じてシェーダ側でブレンドしてやればUnityに搭載されているリフレクションプルーブと同じようなことが出来ます。見栄えを正確にするにはオブジェクトのワールド座標からキューブマップのバウンディングボックスのどの座標にあたるのかを計算することで補正できます BPECMシェーダで検索してみてください。

Referective Cube.js”

// Attach this script to an object that uses a Reflective shader.
  // Realtime reflective cubemaps!

  @script ExecuteInEditMode
  var cubemapSize = 128;
  var oneFacePerFrame = false;
  private var cam : Camera;
  private var rtex : RenderTexture;
  function Start () {
      // render all six faces at startup
      UpdateCubemap( 63 );
  }
  function LateUpdate () {
      if (oneFacePerFrame) {
          var faceToRender = Time.frameCount % 6;
          var faceMask = 1 << faceToRender;
          UpdateCubemap (faceMask);
      } else {
          UpdateCubemap (63); // all six faces
      }
  }
  function UpdateCubemap (faceMask : int) {
      if (!cam) {
          var go = new GameObject ("CubemapCamera", Camera);
          go.hideFlags = HideFlags.HideAndDontSave;
          go.transform.position = transform.position;
          go.transform.rotation = Quaternion.identity;
          cam = go.GetComponent.();
          cam.farClipPlane = 100; // don't render very far into cubemap
          cam.enabled = false;
      }
      if (!rtex) {   
          rtex = new RenderTexture (cubemapSize, cubemapSize, 16);
          rtex.isCubemap = true;
          rtex.hideFlags = HideFlags.HideAndDontSave;
          GetComponent.().sharedMaterial.SetTexture ("_Cube", rtex);
      }
      cam.transform.position = transform.position;
      cam.RenderToCubemap (rtex, faceMask);
      rtex.SetGlobalShaderProperty("_WorldCube");
  }
  function OnDisable () {
      DestroyImmediate (cam);
      DestroyImmediate (rtex);
  }

 



■ リファレンス





回もちょっと長めになってしまいました もと記事を書いたのが2年近く前だったので無駄な文章が散見されますが 頑張って読んで下さってあり難うございます 時間が許せば参考にコードもアップしていけるといいかも。 正直長すぎて途中でよくわからなくなったのでコレで理解出来るのか疑問ですが他のサイトも参考にすればだいじょうぶ…            でしょう

シェーダ周りのめんどうな規則はUnityが汎用エンジンを目指したことで 通常ならライブラリ開発サイドがコントロールする部分をゲーム開発側に対応を任せることになり敷居が上がってしまうというコンセプトの問題点が表面に出てきた感じでしょうか。

スクリプトを前面に押し出すと主なターゲットにするはずだったゲーム開発初心者は腰が引けてしまうのでUnrealEngineのようなうまい感じに目隠しをする方向が理想的な気はします。そこら辺は今後の改良に期待ですか 。。

 

 

今回は前回記事の関連事項となりますが Jsonデータを使用して各ツールでのデータのやりとり関するお話です。

 

すでにあるデータ形式に関しては必要以上に手を加えず サポート外のデータはJsonでやりとりするのが効率が良いという本家フォーラム(英語)のほうでアドバイスを読みまして おおきくはこのような理由が挙げられているようです。

  • Jsonは軽量で各種ゲームエンジン、DCCツール、WebGLなど ほとんどの開発ツールでサポートされているためやりとりに都合が良い
  • データ形式ごとにコンバータを作成する場合 バージョンアップなどのたびに手を入れ続けなければいけないため作業効率が良くない。
  • すでにあるデータ形式の改造を繰り返すことでツール間でデータの互換性が失われてしまうことがある。

 

 

そこで今回はサンプルとしてアンリアルエンジンで提供されているgridExport.mel pythonにリライトし Jsonで書き出しする機能を追加してみました。連番も対応しています。

Pythonを選択する理由は、メジャーなDCCツールではほとんどでPythonに対応されているためです。元がmelで記述されているため一旦pythonでリライトします。

 

 

 

【Python -Maya側】

gridExporter2

gridExportergridExporter3

 

 

『gridExportU.py』

# Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
# Export velocity grid data from a Maya fluid container
# Description: Writes out velocity grid data in a custom formatted ascii file


import pymel.core as pm
import maya.cmds as cmds
import json
from collections import OrderedDict


def gridExportU():
    # Check to see if the UI window already exists. If it does, it is deleted
    if pm.window('gridExport', exists=1):
        pm.deleteUI('gridExport', window=1)
    # Create new UI Window
    pm.window('gridExport', rtf=1, t="gridExport", widthHeight=(300, 300))
    pm.columnLayout('rootLayout')
    pm.frameLayout(marginHeight=5, labelVisible=False, marginWidth=5)
    pm.columnLayout('verticalSubframe')
    pm.setParent('..')
    pm.text(label="UE4 & UnityX Vector Field Exporter")
    pm.radioButtonGrp('myRadBtnGrp', numberOfRadioButtons=2, label="Export Mode",
                      select=1,
                      labelArray2=("Single Frame", "Sequence"),
                      onCommand1=lambda *args: pm.intFieldGrp(
                          'endFrame', edit=1, enable=0),
                      onCommand2=lambda *args: pm.intFieldGrp('endFrame', edit=1, enable=1))

    pm.radioButtonGrp('myRadBtnGrp1', numberOfRadioButtons=2, label="Export Type",
                      select=1,
                      labelArray2=("Fga", "Json"))

    pm.checkBoxGrp('isCached', v1=0, l="Cached Fluid?")
    pm.intFieldGrp('startFrame', l="Start Frame")
    pm.intFieldGrp('endFrame', v1=3, enable=0, l="End Frame")
    #pm.intFieldGrp('increment', v1=1, enable=0, l="Increment")
    pm.textFieldGrp('folderPath', text="C:\\", l="Path")
    pm.textFieldGrp('filename', text="vel", l="Filename prefix")
    pm.columnLayout('exportButton', adjustableColumn=True,
                    columnAttach=("both", 0))
    pm.button(c=lambda *args: iterateExport(), l="export")
    pm.showWindow('gridExport')


def iterateExport():

    startFrame = int(pm.intFieldGrp('startFrame', q=1, v1=1))
    endFrame = int(pm.intFieldGrp('endFrame', q=1, v1=1))
    #increment=int(pm.intFieldGrp('increment', q=1, v1=1))
    dataName = ["velocity"]
    n = startFrame
    sel = pm.ls(sl=1)
    if len(sel) > 1 or len(sel) < 1:
        print "ERROR: Please select a single fluid container \n"

    else:
        fluidShape = pm.listRelatives(sel[0], s=1)
        objectCheck = str(pm.objectType(fluidShape[0]))
        if objectCheck == "fluidShape":
            doit = int(pm.checkBoxGrp('isCached', q=1, v1=1))
            print doit
            if doit == 0:
                sceneCurTime = int(pm.currentTime(q=1))
                sceneMinTime = int(pm.playbackOptions(q=1, minTime=1))
                if sceneCurTime > startFrame:
                    pm.currentTime(sceneMinTime)
                    runupToStart(sceneMinTime, startFrame)

                elif sceneCurTime < startFrame:
                    runupToStart(sceneCurTime, startFrame)

            if pm.radioButtonGrp('myRadBtnGrp', q=1, select=1) == 1:
                pm.currentTime(n)
                folder = str(pm.textFieldGrp('folderPath', q=1, text=1))
                filename = str(pm.textFieldGrp('filename', q=1, text=1))
                filePath = folder + "\\" + filename + "." + str(n)
                # print "Wrote: " + filePath + "\n"

                if pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 1:
                    dataExport(dataName[0], filePath, fluidShape[0])
                elif pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 2:
                    dataExportJson(dataName[0], filePath, fluidShape[0])

            else:
                for n in range(startFrame, (endFrame + 1)):
                    pm.currentTime(n)
                    folder = str(pm.textFieldGrp('folderPath', q=1, text=1))
                    filename = str(pm.textFieldGrp('filename', q=1, text=1))
                    #filePath=folder + "" + filename + "." + str(n) + ".fga"
                    filePath = folder + "" + filename + "." + str(n)
                    # print "Wrote: " + filePath + "\n"
                    if pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 1:
                        dataExport(dataName[0], filePath, fluidShape[0])
                    elif pm.radioButtonGrp('myRadBtnGrp1', q=1, select=1) == 2:
                        dataExportJson(dataName[0], filePath, fluidShape[0])

        else:
            print "ERROR: Please select a fluid container \n"


def dataExport(dataName, filePath, myfluidShape):

    filePath += ".fga"
    voxCount = 0
    # Grab the Grid resolution
    res = map(int, pm.getAttr(myfluidShape + ".res"))
    # switch back to parent transform
    fluidShapeParent = pm.listRelatives(myfluidShape, p=1)
    # Grab the voxel container bounding box
    bb = pm.xform(fluidShapeParent[0], q=1, ws=1, bb=1)
    # create and open the output file in write mode
    fileId = open(filePath, "w")
    # Write voxel res
    fileId.write(("" + str(res[0]) + ","))
    fileId.write(("" + str(res[1]) + ","))
    fileId.write(("" + str(res[2]) + ","))
    # Write bounding Box info
    fileId.write(("" + str(bb[0]) + ","))
    fileId.write(("" + str(bb[1]) + ","))
    fileId.write(("" + str(bb[2]) + ","))
    fileId.write(("" + str(bb[3]) + ","))
    fileId.write(("" + str(bb[4]) + ","))
    fileId.write(("" + str(bb[5]) + ","))
    x = 0
    y = 0
    z = 0

    for z in range(0, res[2]):
        for y in range(0, res[1]):
            for x in range(0, res[0]):
                v = pm.getFluidAttr(xi=x, yi=y, zi=z, at=dataName)

                fileId.write(
                    (str(v[0]) + "," + str(v[1]) + "," + str(v[2]) + ","))

    fileId.close()


def runupToStart(baseframe, exportFirstFrame):

    i = 0
    for i in range(baseframe, exportFirstFrame):
        # print($i+"...\n")
        pm.currentTime(i)


def dataExportJson(dataName, filePath, myfluidShape):

    filePath += ".json"

    voxCount = 0

    # Grab the Grid resolution

    res = map(int, pm.getAttr(myfluidShape + ".res"))
    # switch back to parent transform
    fluidShapeParent = pm.listRelatives(myfluidShape, p=1)
    # Grab the voxel container bounding box
    bb = pm.xform(fluidShapeParent[0], q=1, ws=1, bb=1)
    # create and open the output file in write mode

    x = 0
    y = 0
    z = 0
    dict = []

    for iz in range(0, res[2]):
        for iy in range(0, res[1]):
            for ix in range(0, res[0]):
                v = pm.getFluidAttr(xi=x, yi=y, zi=z, at=dataName)
                myVelocity = {"x": v[0], "y": v[1], "z": v[2]}
                dict.append({"velocity": OrderedDict(
                    sorted(myVelocity.items()))})

    exportData = {
        'metadata': {
            'formatVersion': 1.0,
            'generatedBy': 'VectorFieldExporter'
        },
        "Data": {
            "Resolution": OrderedDict(sorted({"resX": res[0], "resY": res[1], "resZ": res[2]}.items())),
            "BBOX": {
                "Min": OrderedDict(sorted({"minX": bb[0], "minY": bb[1], "minZ": bb[2]}.items())),
                "Max": OrderedDict(sorted({"maxX": bb[3], "maxY": bb[4], "maxZ": bb[5]}.items()))
            }
        }, "DataArray": dict

    }

    writeJsonFile(exportData, filePath)


def writeJsonFile(dataToWrite, fileName):
    if ".json" not in fileName:
        fileName += ".json"

    print "> write to json file is seeing: {0}".format(fileName)

    with open(fileName, "w") as jsonFile:
        json.dump(dataToWrite, jsonFile, indent=4, separators=(',', ': '))

    print "Data was successfully written to {0}".format(fileName)

    return fileName


gridExportU()

 

GUI部分を除けばJsonのエクスポートをする部分とgetAttrを使用してシーンからデータを配列にコピーするだけなので、Maya以外に対応させるときは、使用するツールごとになんらかのエクスポーターを参考にしてJsonデータを書き出しする箇所を追記するだけで同様なスクリプトが書けると思います。

”import json” でJsonモジュールを読み込んで key[ ]とValue[ ]を別々に配列として登録した後に zip(key,value)で辞書型配列に登録するのが一般的な書き方のようなのですが、データの整列部分をはさんで今回は直接データを辞書型に変換しています。

通常の配列を辞書型の配列に変換すると要素の順番が保持されないため これを整列するためにOrderedDictモジュールをインポートしています。 これによってアルファベット順に辞書型配列内のデータを整列させて読みやすくなります ただしJsonをインポートするときには構造体のKeyを参照して読み込まれるためデータ順はどのようになっていても良いので この部分は無くてもかまいません。

  

OrderedDict(sorted({"resX": res[0], "resY": res[1], "resZ": res[2]}.items()))

 

”Python側”のexportDataが書き出しデータを整形するための配列となります。、カスタマイズしたい場合はこの配列内の記述を参考に、書き出されたデータの対応は”Python側” ”Json側"のスクリプトを参考にしてください

python(json)での表記は { }:辞書型配列 、[ ]:リスト配列型、():タプル型 となります。

Jsonファイルに書き出しをする部分は以下のように

 

with open(fileName, "w") as jsonFile:
        json.dump(dataToWrite, jsonFile, indent=4, separators=(',', ': '))

 

それぞれ 書き出しは json.dump( )   読み込みは json.loads( ) を使用します

参考:YoheiM.NET:[Python] JSONを扱う http://www.yoheim.net/blog.php?q=20150901

 

”Python側”

  exportData = {
        'metadata': {
            'formatVersion': 1.0,
            'generatedBy': 'VectorFieldExporter'
        },
        "Data": {
            "Resolution": OrderedDict(sorted({"resX": res[0], "resY": res[1], "resZ": res[2]}.items())),
            "BBOX": {
                "Min": OrderedDict(sorted({"minX": bb[0], "minY": bb[1], "minZ": bb[2]}.items())),
                "Max": OrderedDict(sorted({"maxX": bb[3], "maxY": bb[4], "maxZ": bb[5]}.items()))
            }
        }, "DataArray": dict

    }

”Json側"

{
    "Data": {
        "Resolution": {
            "resX": 30,
            "resY": 30,
            "resZ": 30
        },
        "BBOX": {
            "Max": {
                "maxX": 5.0,
                "maxY": 5.0,
                "maxZ": 5.0
            },
            "Min": {
                "minX": -5.0,
                "minY": -5.0,
                "minZ": -5.0
            }
        }
    },
    "DataArray": [
        {
            "velocity": {
                "x": -0.0008264329517260194,
                "y": 0.03183583915233612,
                "z": 0.014935646206140518
            }
        },
        {
            "velocity": {
                "x": -0.0008264329517260194,
                "y": 0.03183583915233612,
                "z": 0.014935646206140518
            }

 

   …………

 

],
    "metadata": {
        "formatVersion": 1.0,
        "generatedBy": "VectorFieldExporter"
    }
}

 

 

【C# Unity側】

『 SerializeBuffer.cs 』

using UnityEngine;
using System.Collections;
using System.IO;
using System.Collections.Generic;


[System.Serializable]
public class HeaderData
{
    public Vector3 Res;
    public Vector3 Max;
    public Vector3 Min;
}

[System.Serializable]
public class TransformData
{
    public Vector3 velocity;
}

[System.Serializable]
public class HeaderCollection
{
    public HeaderData[] HeaderArray;
}

[System.Serializable]
public class TransformCollection
{
    public TransformData[] DataArray;
}


public class SerializeBuffer : MonoBehaviour
{  
    public int texWidth = 1024;
    public int texHeight = 1024;


    public Vector3 Resolution;
    public Vector3 BBOX_Min;
    public Vector3 BBOX_Max;

    
    public string _FilePath = "Assets/Resources/";
    public string _FileName = "TexturedBuffer.png";

   
    public HeaderCollection MyHeaderData;
    public TransformCollection MyData;
    public const int MaxFileNum = 10;

    TransformCollection[] MyDataList = new TransformCollection[ MaxFileNum]; 
  



    void Awake()
    { 
    
        MyHeaderData = new HeaderCollection();
        MyData = new TransformCollection();
  

    }

    public void ConvertData()
    {
       
        string JSONString;
   
        DirectoryInfo dir = new DirectoryInfo(_FilePath);
        FileInfo[] info = dir.GetFiles("*.json");

        var index = 0;

        foreach (FileInfo f in info)
        {
                  
            JSONString = File.ReadAllText(f.FullName); // FileInfo.Name  or FileInfo.FullName
            Debug.Log("LoadPath: " + f);

            MyHeaderData = JsonUtility.FromJson(JSONString);
            MyData = JsonUtility.FromJson(JSONString);
            MyDataList[index] = MyData;
               
            index++;
           
        }
           
        Texture2D texture = new Texture2D(texWidth, texHeight, TextureFormat.RGB24, false, false);
        texture.filterMode = FilterMode.Point;
        texture.wrapMode = TextureWrapMode.Clamp;

        texture.Apply();

        int mip = 0;
        int maxIndex = (int)Mathf.Ceil(MyData.DataArray.Length / texWidth) * texWidth;

        Color[] cols = new Color[3];
        cols = texture.GetPixels(mip);

        int offset = 0;

        for (int j = 0; j < info.Length; j++)
        { 
           
            offset = j * maxIndex;

            for (int i = 0; i < maxIndex; ++i)
            {
               
                if (i < MyData.DataArray.Length)
                {

                    cols[i + offset].r = MyDataList[j].DataArray[i].velocity.x;
                    cols[i + offset].g = MyDataList[j].DataArray[i].velocity.y;
                    cols[i + offset].b = MyDataList[j].DataArray[i].velocity.z;

                }
                else
                {
                    cols[i + offset].r = 0;
                    cols[i + offset].g = 0;
                    cols[i + offset].b = 0;
                  
                }
                            
            }

        }

        texture.SetPixels(cols, mip);
         
        string saveFilePath = Path.Combine(_FilePath, _FileName);
        byte[] bytes = texture.EncodeToPNG();
        File.WriteAllBytes(saveFilePath, bytes);

        DestroyObject(texture);
  

       
        Resolution = MyHeaderData.HeaderArray[0].Res;
        BBOX_Min = MyHeaderData.HeaderArray[0].Min;
        BBOX_Max = MyHeaderData.HeaderArray[0].Max;

        Debug.Log("Textured_Buffer Exported : " + saveFilePath);
        Debug.Log("Resolution:" + MyHeaderData.HeaderArray[0].Res);
        Debug.Log("BBOX Min:" + MyHeaderData.HeaderArray[0].Min);
        Debug.Log("BBOX Max:" + MyHeaderData.HeaderArray[0].Max);

        Debug.Log("\n Buffer Export Completed ! ");
    }


    //------------------------------------------
    void Update()
    {
         if (Input.GetKeyDown(KeyCode.Space))
        {
            ConvertData();
            return;
        }
    }

}

 

動作は単純で指定したフォルダ内にあるJsonファイルを全て読み込んでデータをテクスチャに変換するだけです。

使用方法はJsonファイルへのフォルダパスと書き出すテクスチャの名前を設定した後スペースキーを押してください Jsonファイルと同じフォルダパスにTextureBufferのPngデータが書き込まれます。

当初サブフォルダまでサーチする仕様でしたが、作業中にサブフォルダに一旦ファイルを避けておくような用途も考えてやめました。

コマ抜きやら順序入れ替えやらのオプション操作も考えましたがwindows上で必要なファイルだけを選ればいいでしょうしツールは単純な方が使い勝手が良いので必要なしと判断しました。

 

Jsonから読み込んだデータの内容は以下の通り

        Resolution (バッファの解像度) MyHeaderData.HeaderArray[ ].Res 
        BBOX_Min (バウンディングボックス最小座標) MyHeaderData.HeaderArray[ ].Min 
        BBOX_Max (バウンディングボックス最大座標) MyHeaderData.HeaderArray[ ].Max

        velocity.x (x要素)           MyDataList[ ].DataArray[ ].velocity.x; 
        velocity.y (y要素)           MyDataList[ ].DataArray[ ].velocity.y; 
        velocity.z (z要素)            MyDataList[ ].DataArray[ ].velocity.z;


DataArray[ ]内の要素がvelocity.X、velocity.Y、velocity.Z、でMyDataList[ ]がDataArray[ ]をJsonの数だけ格納されています。

MyHeaderDataは解像度は共通なため一つ分だけ存在します。

 

 

【テクスチャデータについて】

【TextureBuffer.png】

TexturedBuffer

 

テクスチャへのデータのパックはUnity側はGL系なのでUV座標のV方向がフリップしています そのため見た目で下から上に向かって配置されます。 シェーダ側でDirectXを使用する場合はV方向は反転されるためテクスチャは上下が反転した見た目となります。

Velocityのデータはインポートした時すでに0~1の長さに正規化されているので、Color型配列にそのまま代入してテクスチャに書き込んでいます。

1以上の範囲を取るデータの場合はな正規化してから代入してください。詳しくは 以前の配列の転送に関する記事を参考にしてください。

 

【ファイルパス操作について】

セーブファイル名のパスとファイル名の連結にPathクラスを使用しています。

 

Path.Combine(_FilePath, _FileName);

 

Pathクラスの標準関数はマニュアルと同じですが次の通り

  • Combine                                   2つのパスストリングを結合してファイルパスに変換。
  • GetDirectoryName                       ディレクトリパスを返す.
  • GetExtension                              ファイルの 拡張子を返す.
  • GetFileName                              拡張子を含めたファイルネームを返す.   
  • GetFileNameWithoutExtension    拡張子抜きのファイルネームを返す.

    Path.GetFileNameWithoutExtension("/Some/File/At/foo.extension"));
    Path.GetFileName("/Some/File/At/foo.extension"));
    Path.GetDirectoryName("/Path/To/A/File/foo.txt"));
    Path.GetExtension("/Some/File/At/foo.extension"));
    Path.Combine("/First/Path/To", "Some/File/At/foo.txt"));

Pathクラスのvalue

  • AltDirectorySeparatorChar       ディレクトリレベルを区切るための代替文字。 (Read Only) '/' Windows 、 '/'  macOS
  • DirectorySeparatorChar           ディレクトリレベルを区切るために使用されるデフォルトの文字。 (Read Only) '\' Windows 、 '/'  macOS


 

 

【その他】

"maya-json-exporter" https://github.com/Jam3 https://github.com/Jam3/maya-json-export

Ttree.js など MayaのJsonエクスポータはいくつか公開されていますので参考にすると良いかと思います。

melとPython両方に対応されていますが、pythonがOpenMayaで記述されている箇所があり、非プログラマには若干ハードルが高いかもしれないです。 今回の解説をPython+Pymelだけで書いた理由でもあります 他プラットフォームへの移植もめんどうですし。

データが大きく 処理時間がかかりすぎるようであればOpenMayaかC++で書き換えする必要があるかもしれませんが、データをGetする以外は特に重たい処理をしているわけでもないので、わりと速度が気になるほどではないような気がします。

現在すでに手が入っている部分はそのままでよいかと思いますが ちょっとデータが必要だけどコンバータを書くのがめんどくさいとか言う場合に手軽にデータを送ることができるので便利かもしれません。

 

シェーダ側での配列の扱いについては、次回以降 余裕があれば、ぼちぼち書いていく 予定。

 

ではまた

 

更新がチョットあいてしまいましたが シェーダ配列関連の検索が多いようですので以前の記事からの変更点をまとめてみます。

Alan Zucconi氏 『Arrays & Shaders in Unity 5.4+』という記事がよくまとまっていますので翻訳と追加情報をすこし加えて記事にしてみました。

元記事のAlan Zucconi氏は

『Unity 5.x Shaders and Effects Cookbook − Alan Zucconi (著), Kenneth Lammers (著)』 の著者の方です。

 

 

■Arrays & Shaders in Unity 5.4+

http://www.alanzucconi.com/2016/10/24/arrays-shaders-unity-5-4/

 

この記事は、Unity 5.4で配列とシェーダを使用する方法を示しています。2016年1月に私はすでに 『Unityでヒートマップ:配列およびシェーダ』と呼ばれる記事でこのトピックをカバーしています 元のアプローチでは、配列をシェーダに渡すことができる文書化されていない機能が公開されていました。それ以来、Unity 5.4はAPIに適切なサポートを導入しました。このチュートリアルは、以前の記事を置き換えるものです。以前のチュートリアルを読んでいればシェーダコードを変更する必要はなくステップ2に進むことができます。

 

  • ステップ1. シェーダ
  • ステップ2. スクリプト
  • ステップ3.  制限事項
■ ステップ1.シェーダ
すべてのシェーダにはプロパティと呼ばれるセクションがあり、マテリアルインスペクタ内の特定の変数を公開することができます。この記事が書かれた時点では、Unityは配列型をサポートしていません。したがって、インスペクタから直接配列にアクセスすることはできません。Unityが2DArrayと呼ばれるタイプをサポートしていますが、これはテクスチャ配列用に予約されています。私たちが望むのは、数字の配列です。

すべての配列は、変数として宣言されスクリプトを介して外部的に初期化されなければなりません。シェーダ内の配列は、あらかじめ定義された長さを持つ必要があります。格納する必要のあるアイテムの数を事前に把握していない場合は、実際にいくつのアイテムが存在するかを示す変数(配列の長さなど _ArrayLength)を保持してスペースを確保します。

 

int _ArrayLength = 0;

float _Array[10];

上記の例では、両方の変数uniformのキーワードが施されています。これは、その値が外部スクリプトから変更され、それらの変更がフレーム間で発生しないと仮定しているためです。

他のタイプの配列と同様にシェーダコードで配列を見ることができます:

 

for (int i = 0; i < _Length; i++){

...

float x = _Array[i];

...

}

■ ステップ2.スクリプト

シェーダを使用する場合は、外部スクリプトを使用して配列を初期化する必要があります。ユニティ5.4+の新しいAPIでは、SetFloatArray、SetMatrixArray  とSetVectorArrayをサポートしています。予想されたように、それらの配列を初期化するためにfloat、Matrix4x4  とVector4がそれぞれ使用されています。これは これらの関数を正しく使用するためのスニペットです。

 

float [] array = new float[] { 1, 2, 3, 4 };

material.SetFloatArray(array);

ここでのマテリアルは  あなたのシェーダで使用するUnityのマテリアルです。インスペクタから直接ドラッグすることも、コードで取得することもできます。

 

Renderer renderer = GetComponent();

Material material = renderer.shaderdMaterial;

Unity 5.4はグローバル配列もサポートしています。それらは一度設定されればその後すべてのシェーダによって共有されるプロパティです。それらは、同様の方法で動作し、 SetGlobalFloatArray、SetGlobalMatrixArray  とSetGlobalVectorArrayといったシグネチャを持っています。しかしながらこれらはシェーダのの静的メソッドのクラスです。

 

v5.4以降で 新しく追加されたarray転送命令が次の関数

 

■ ステップ3.制限事項

あなたが(例えば、他のタイプの配列を渡す必要がある場合はint型、long型、Vector3、...)をあなたが密接にあなたのニーズに合った方法を使用する必要があります。たとえば、あなたがint型をシェーダの配列にあわせる場合  配列内の値をfloatに変換しなければなりません。同様にあなたがVector3をシェーダに割り当てたい場合Vector3をVector4でラップしなければなりません。あなたがVector3をVector4に自動的に割り当てることができるときは Unityが自動的に最後の座標0をセットして、適切にそれらが収まるようにします。ただし、Vector3[ ] を  Vector4 [ ] に割り当てることは出来ません。

2番目に考慮すべきことは、Unityによって行われた設計の選択肢が貧弱であることです。初めて(ローカルまたはグローバルに関係なく)配列を使用すると、Unityは配列自体のサイズを固定しているようです。たとえば、あなたのようにシェーダーで定義された配列の初期化にuniform float _Array[10]; 。 以下のように定義されたC#の配列を持つ float[] array = new float[5]; 。配列に5つ以上の要素を設定することはできません。これがバグかフィーチャーかに関わらず、非常に厄介なバグがあります。これが修正されるのを待って、スクリプトのAwake関数で直接最大サイズで配列を初期化するようアドバイスします:

 

void Awake () {

material.SetFloatArray("_Points", new float[10]);

}

※ 一部のユーザーから 配列が初期化されたら サイズをリセットできるようにエディタを再起動する必要があると報告されています。

 

 

 

■Unity5.4+以降のシェーダ配列周りの変更による考察

https://forum.unity3d.com/threads/passing-array-to-shader.392586/

 

■MaterialPropertyBlock

Unity - Scripting API- MaterialPropertyBlock

シェーダのインスタンシング描画がサポートされましたのでMaterialPropertyBlockによる配列操作もサポートされました。

MaterialPropertyBlockの記述方法が2種類あり 設定した変数名を直接指定する場合と nameIDを指定する場合でnameIDはより高速に実行できます。 ここらへんは以前からある命令ですがセットで覚えると理解しやすいので一応

 

■スクリプト側 <マニュアルから抜粋>

『シェーダで設定した変数名を使用する場合』

using UnityEngine;

// Draws 3 meshes with the same material but with different colors. public class ExampleClass : MonoBehaviour { public Mesh mesh; public Material material; private MaterialPropertyBlock block;

void Start() { block = new MaterialPropertyBlock(); } void Update() { // red mesh block.SetColor("_Color", Color.red); Graphics.DrawMesh(mesh, new Vector3(0, 0, 0), Quaternion.identity, material, 0, null, 0, block);

// green mesh block.SetColor("_Color", Color.green); Graphics.DrawMesh(mesh, new Vector3(5, 0, 0), Quaternion.identity, material, 0, null, 0, block);

// blue mesh block.SetColor("_Color", Color.blue); Graphics.DrawMesh(mesh, new Vector3(-5, 0, 0), Quaternion.identity, material, 0, null, 0, block); } }

『nameIDを使用する場合』

using UnityEngine;

// Draws 3 meshes with the same material but with different colors. public class ExampleClass : MonoBehaviour { public Mesh mesh; public Material material; private MaterialPropertyBlock block; private int colorID;

void Start() { block = new MaterialPropertyBlock(); colorID = Shader.PropertyToID("_Color"); } void Update() { // red mesh block.SetColor(colorID, Color.red); Graphics.DrawMesh(mesh, new Vector3(0, 0, 0), Quaternion.identity, material, 0, null, 0, block);

// green mesh block.SetColor(colorID, Color.green); Graphics.DrawMesh(mesh, new Vector3(5, 0, 0), Quaternion.identity, material, 0, null, 0, block);

// blue mesh block.SetColor(colorID, Color.blue); Graphics.DrawMesh(mesh, new Vector3(-5, 0, 0), Quaternion.identity, material, 0, null, 0, block); } }
 

 

1つのシェーダに複数の配列を持つことができます。この制限は、ハードウェアよりも内部実装によるものです。定数バッファに配列を置くと、定数バッファの最大サイズ(D3D11では64KB、OpenGLでは16KB)によっても制限されます。

■ シェーダ側でコンスタントバッファ(定数バッファ)をネームアップする場合 定数バッファの最大サイズ(D3D11では64KB、OpenGLでは16KB)によっても制限されます。これは CBUFFERをいくつかの塊に分けて記述することで回避します。

※ 未使用のバッファが存在すると処理速度の低下につながりますので、リリース時にはチェックしておきましょう。

 
UNITY_INSTANCING_CBUFFER_START(MyProperties)
                    UNITY_DEFINE_INSTANCED_PROP(float4, _Color)
                    UNITY_DEFINE_INSTANCED_PROP(float4x4, _m)
UNITY_INSTANCING_CBUFFER_END


UNITY_INSTANCING_CBUFFER_START(MyProperties1)

                    ……
                    ……
UNITY_INSTANCING_CBUFFER_END


UNITY_INSTANCING_CBUFFER_START(MyProperties2)
                    ……
                    ……
UNITY_INSTANCING_CBUFFER_END

 

■その他Tipsなど

■通常のテクスチャを使用する場合のTips:

シーン全体で同じテクスチャ配列を設定する場合は 配列のGrobal化と同様にShader.SetGlobalTextureを使用します。

 

Texture配列からデータの読み取りをする場合はUnity標準APIではGetPixels()命令などを使用することになるのですが、御存知の通りたいへんな低速です。 ただし128×128程度のサイズであればそれなりに使えなくもないないようです。

テクスチャが格納されているメモリはCPUとGPUに分かれていますが、GetPixels()という関数は発生した次点でGPUでレンダリングされているテクスチャをCPU側にコピーしようとするため レンダリングが途完了していない場合タスクが非常に重くなるという欠点があります。

IEnumeratorを使用してテクスチャレンダリングが終了しているかのチェックとフレームエンドのチェックを行うことである程度速度は向上できますがどちらにしろお勧めはしません。 < if( render_texture )  … if( End of Frame ) みたいな感じで チェックしますがテクスチャのチェックだけでもいけます>

以前に参照した記事によれば、スマホなどDirectXを使用しない環境であればOpenGLモードで起動(Unity起動バッチにforce-openGL オプション)

"C:\Program Files (x86)\Unity\Editor\Unity.exe" -force-opengl

GL関数のglReadPixels()する方法が有効かもしれないということです。

C++などでプラグインからGLをたたく事もできますが、マルチプラットフォーム開発の場合プラグインの調整の時間が取られることから これもあまりお勧め出来ないとか。

 

5.4以降で すでにシェーダ側と配列をやり取りする機能がありますので texture配列は用途が少なくなりそうですが GPU計算のスワップバッファなどの描画周りでは用途がありますのでそこら辺の解説は後日、

 

 

■TextureArrayの追加もありました

hlslの命令のサポートになりますが通常のtextureとほぼ同じもので2Dスプライトのアトラスを実装するためにサポートされたようです。

  • TextureとTextureArrayは 両方共アクセス速度は差がないのでTextureで配列の転送をしている場合は特に変更する必要はないようです。
  • TexutreArray はDX9世代(SM3.0以前)には対応していないので注意が必要です。
  • 内部的にはTexture1Dもサポートしているふうなんですが 本家のフォーラムでUnityのエンジニアの方が Texture2Dで代用しても速度的には変わらないので2D を使用してもらいたいという 発言がありました。 最適化の記事でよく1DTextureは2DTextureより高速なので積極的に使用すべきなどとある という質問に対しての回答です。

 

使用するファンクションは通常のテクスチャと同様に

  • Apply Actually apply all previous SetPixels changes.
  • GetPixels Returns pixel colors of a single array slice.
  • GetPixels32 Returns pixel colors of a single array slice.
  • SetPixels Set pixel colors for the whole mip level.
  • SetPixels32 Set pixel colors for the whole mip level.

などのファンクションと テクスチャイメージをコピーするGraphics.CopyTexture()が使用できます。

■ Graphics.CopyTexture()のサンプル

https://forum.unity3d.com/threads/how-do-you-use-graphics-copytexture.428008/

//Graphics.CopyTexture()

void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
       // Texture2D dest = new Texture2D(Screen.width, Screen.height);
        for (int i = 0; i < charData.charData.Length; i++)
        {
            Rect r = charData.GetNormalizedRectFromId(charData.charData[i].id);
            float scale = 1f;
            Rect DestRect = new Rect(charData.charData[i].x * scale, charData.charData[i].y * scale, charData.charData[i].width * scale, charData.char Data[i].height * scale);
//            Graphics.DrawTexture(DestRect, charData.FontImage, r, 0, 0, 0, 0, Color.red, renderMat);
            Graphics.CopyTexture(charData.FontImage, 0, 0,
                charData.charData[i].x,
                charData.charData[i].y,
                charData.charData[i].width,
                charData.charData[i].height, dest, 0, 0, charData.charData[i].x, charData.charData[i].y);
        }
        Graphics.Blit(dest, destination,renderMat);
    }

 

■ Sample2DArrayTexture

マニュアルのサンプルコードと同じものです

Shader "Example/Sample2DArrayTexture"
{
    Properties
    {
        _MyArr ("Tex", 2DArray) = "" {}
        _SliceRange ("Slices", Range(0,16)) = 6
        _UVScale ("UVScale", Float) = 1.0
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // to use texture arrays we need to target DX10/OpenGLES3 which
            // is shader model 3.5 minimum
            #pragma target 3.5
           
            #include "UnityCG.cginc"

            struct v2f
            {
                float3 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            float _SliceRange;
            float _UVScale;

            v2f vert (float4 vertex : POSITION)
            {
                v2f o;
                o.vertex = mul(UNITY_MATRIX_MVP, vertex);
                o.uv.xy = (vertex.xy + 0.5) * _UVScale;
                o.uv.z = (vertex.z + 0.5) * _SliceRange;
                return o;
            }
           
            UNITY_DECLARE_TEX2DARRAY(_MyArr);

            half4 frag (v2f i) : SV_Target
            {
                return UNITY_SAMPLE_TEX2DARRAY(_MyArr, i.uv);
            }
            ENDCG
        }
    }
}

 

【関連記事】

 

 

今回は以上ですが、また新しい情報があれば追記するかもしれまん。

体調がすぐれないのでこの辺で... ではまた





どうもこんにちは 松の内も過ぎてしまいましたが


『あけました おめでとうございます』 


「あいさつが遅れましたが、年明け初収録なので大丈夫です」
というネットラジオのよくわからない言い訳を聞いていたら
遅れても大丈夫な気がしてきました。


近況的なものをすこし


年末から 風邪をひいてなかなか治らないのですね 

一日中ぐったりモードで思うように動けず とりあえず
今期アニメを消化中ですが アホみたいに本数が多いので、
さらに体力が削られてゆくという 悪循環。


そんな暇があったら 寝れ。 と自分に言い聞かせています。


作業効率のためには睡眠は超重要なので無駄時間があったら寝て
体力を回復しましょう。 

医学的には睡眠中は脳内でチェックプログラムが走っている状態で
メモリの整理がおこおなわれるため記憶の定着に有効 
また成長ホルモンの分泌が活発化して内臓の修復が行われるということです。
 
若いときに徹夜自慢していた人がコロッといくのを思い当たる人も多いのでは?

ちなみに島本和彦先生の締切が迫っていても『あえて寝る』戦法は気がつくと
締め切りを大幅にオーバーしている上級者向きでたいへん危険な技です。

あたりまえ 



今後の予定です

閲覧の多いゲーム開発系の記事は、月末あたりアップを目指そうかなと思ってます。

スクリプトがなかなかフィックスできなくてちょっと困っている最中で
間に合わなければ また駄日記でお茶を濁すかもしれません (笑

現在はUnityやらUE4やらいろいろと研究を進めてはいるのですが
とりあえず方向性がブレないように このブログはUnity関連の記事で 
UE4の記事は裏サイトの方かなーと考えているのですが 気まぐれゆえ
どうなるかはわかりません。

UE4はC++が久しぶりに役に立っていてうれしいかも C++の仕事ってあんまりないからね
アプリかゲームぐらいかな 仕事の多さではWEB系のスクリプトが圧倒してますし。
javaスクリプトとかPythonもWeb関連のお仕事で使用頻度が高くて書けてよかった言語でした。



ということで

皆様も風邪にお気をつけて ではまた 



追記:

そういえば 先程コーヒーを買ってきましたが
微糖が欲しかったのですけど間違って無糖を買って来てしまいました
「あー 微糖 が欲しかったのに 無糖 買っちゃたよー」というそんな時
頭のなかで武藤敬司と尾藤イサオの雑コラが想い出される現象に
何か名前をつけるべきではと考えましたが、いいネーミングが思いつきません。

↑このページのトップヘ