Yaminabe

らくがきぶろぐ

てすと: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のため50のヒントとベストプラクティス”の2016年改訂版の意訳をしてみました。
誤訳また記述ミス、その他があるかもしれませんが、それらはコメント欄のほうにお願いします。
 

 

■ Unityのため50のヒントとベストプラクティス(2016年版)

Gamasutra:ハーマンTullekenのブログ - Unityのため50のヒントとベストプラクティス(2016年版)


約4年前、私はオリジナルである 50 Tips for working with Unity公開しました

オリジナルバージョンとは多くの関連がありますが、その後に多数の変更があります:

  • Unityは良くなりました。たとえば、私は現在、FPSカウンタを信頼しています。 PropertyDrawersを使用できるため、カスタム・エディターを書く必要が少なくなります。 プレハブ作業を行う方法は、明示的にネストされたプレハブを必要とし ScriptableObjectは相性がいいです。
  • Visual Studioの統合性が向上しました。デバッグが非常に簡単になり、ゴリラデバッグが不要になりました。
  • サードパーティ製のツールとライブラリが良くなりました。Asset Storeには、視覚的なデバッグやより良いロギングなどの処理を行う多くのアセットが用意されています。。私たち独自の(無料)拡張機能は、プラグインは、元の記事に記載された元の記事に記載されているコードの多くが含まれています。(それの多くは、ここでも説明します)。
  • バージョン管理は良くなりました。(あるいは、今、私はそれを効果的に使用する方法を知っています)。例えば、複数のコピーやプレハブのバックアップコピーを持つ必要はありません。
  • 私はより多くの経験を得ました。私はこの4年間で、数多くのゲームのプロトタイプFather.IOのようなプロダクションゲーム および当社の主力Unityアセットグリッド。などの数多くのUnity・プロジェクトに取り組みました。

    この記事は、上記をすべて考慮したオリジナルの改訂版です。




Tipsのディスカッションを続ける前に、私たちはここに次の免責条項を発行します(初期バージョンと基本的に同じです):

これらのヒントは、すべてのUnityプロジェクトに適用されるものではありません。

  • これらは、3?20人からの小さなチームのプロジェクトでの私の経験に基づいています。
  • チームのサイズ、プロジェクトの規模、およびプロジェクトの目標により その代価が支払われるべきかどうかを決定します 。構造、再利用性、透明性、などの評価値があります 例えば あなたはゲームジャム中にすべてのこれらを使用することはありません、。
  • 多くのヒントは好みの問題です(ここに記載されているTipsに匹敵する技術があるかもしれません)。

以下の記事にもいくつかのUnityのベストプラクティス(パフォーマンスの観点からのものが、ほとんどですが)が存在します:


ワークフロー開発プロセス)

1.最初に決定した規模ですべてのプロトタイプを構築します。

そうしないと(たとえば、あなたが常に正しくアニメーションをスケーリングすることはできません)、その後のアセットをやり直す必要があるかもしれません。1Unity・ユニットを使用して3Dゲームの場合、= 1メートル、通常は最高です。照明や物理学を使用していない2Dゲームのために、(「デザイン」の解像度で)1Unity単位= 1ピクセルは通常は良いです。UI(および2Dゲーム)について、すべての資産がその解像度にスケーリングするために設計解像度(私たちはHDまたは2xHDを使用)とデザインを選択します。

2. すべてのシーンが実行可能にしてください。

これによりシーンを切り替えてゲームを実行する必要がなくなり、より速くテストできるようになります。すべてのシーンで必要なシーン読み込みの間にオブジェクトが残っている場合、これは難しいことです。これを実行する1つの方法は、彼らがシーンに存在しないときに自分自身をロードする永続オブジェクトのシングルトンを作る使用することです。シングルトンは別のTipsでより詳細に記載されています。

3. ソースコード管理と効果的にそれを使用する方法を学びます。

  • アセットをテキストとしてシリアル化します。

    シーンとプレハブをより合併させることは実際にはありませんが、変更されたものをより見やすくします。

  • シーンやプレハブ共有戦略を採用します。

    一般的には、複数の人がシングルトンは別のチップでより詳細に記載されています。同じシーンやプレハブでは動作しないようにしてください。小さな制作チームのために、誰もがシーンやプレハブを作ることができないことを保証するために作業を開始する直前に。所有権のシーンを代表する取引所の物理的なマークは、(デスクトップ上のシーンがある場合は、あなただけの1シーンで作業することができます)有用である可能性があります。

  • ブックマークなどのタグを使用してください。
  • 分岐戦略を決めて固執する。 シーンとプレハブをスムーズにマージすることはできないので、分岐はやや複雑です。 あなたがブランチを使うことに決めた場合は、あなたのシーンとプレパブの共有戦略にそって動作しなければなりません。
  • サブモジュールを使用する場合は注意してください。

    サブモジュールは、再利用可能なコードを維持するための素晴らしい方法ですが、いくつかの注意事項があります。

  • メタファイルは、一般的に複数のプロジェクトにわたって一貫していません。非Monobehaviourまたは非Scriptableオブジェクトコードでは一般的に問題ではありませんが、サブモジュールを使用するMonoBehavioursおよびScriptableオブジェクトでは、コードが失われる可能性があります。
  • 多くのプロジェクト(サブモジュールで1つ以上を含む)で作業している場合、すべてのプロジェクトでコードを安定化させるために、さまざまなプロジェクトをプル・マージ・コミット・プッシュする必要がある場合には、アップデート・アバランシェを得ることがあります これが起こっている間に他の誰かが変更を加えている場合、それは持続的な雪崩に変わる可能性があります)。この影響を最小限に抑える1つの方法は、専用のプロジェクトからサブモジュールを常に変更することです。このように、サブモジュールを使用するプロジェクトは、常にプルする必要があります。それらはプッシュバックする必要はありません。

4.テストシーンやコード別々に保管してください

リポジトリに一時的なアセットやスクリプトをコミットし 編集しているプロジェクトから削除します。

5.ツール(特にUnity)をアップグレードする場合は同時にインストールしてください。

Unityは、以前とは異なるバージョンのプロジェクトを開いたときにリンクを保持することに大変優れていますが、各メンバーが異なるバージョンで作業しているときにリンクが失われることがあります。

6. サードパーティ製のアセットはクリーンなプロジェクトにインポートし独自に使用するための新しいリソースパッケージをエクスポートしましょう。

プロジェクトに直接リソースをインポートすると、それらは時々問題を引き起こす可能性があります:

  • 競合(ファイルまたは名前)、特に、プラグインフォルダのルートにファイルがあるか、サンプルの標準アセットのアセットを使用するアセットが存在する可能性があります。
  • あなた自身のプロジェクト全体にファイルを置くときに組織化されていないかもしれません。。これは特に、使用しないで削除する場合に問題になります。

より安全にアセットをインポートするには、以下の手順に従ってください:

  1. 新しいプロジェクトを作成し アセットをインポートします。
  2. examples を実行し それらが動作することを確認してください。
  3. アセットは、より適切なディレクトリ構造に配置されています。 (私は通常、独自のディレクトリ構造のリソースの強制的なアライメントないんだけど、私は、任意のプロジェクトのないディレクトリ内のすべてのファイルは、既存のファイルは重要な位置に存在する上書きされる可能性がありますことを確認しています。)
  4. examplesを実行して動作確認をします。(リソースを移動したときに時々 アセットの損傷を引き起こす可能性がありますが、一般的に問題になることはありません)。
  5. 必要のないものを全て削除します(たとえばexamplesのような)。
  6. アセットがコンパイルされているか そしてプレハブが全てリンクされているか確認します。なにか残っているかを実行してテストします
  7. すべてのアセットを選択して、パッケージをエクスポートします。
  8. プロジェクトにインポートします。

7. ビルドプロセスの自動化。

これは小規模なプロジェクトでも役に立ちますが、以下の場合に特に便利です:

  • 多くの異なったゲームのバージョンでビルドが必要な場合。
  • チーム内の技術知識の差異があるメンバーでビルドの必要がある場合。
  • プロジェクトのビルド前に微調整をする必要がある場合。

Unity Builds Scripting: Basic and advanced possibilities 詳しくはリンク先を参照してください。


8. ドキュメントセットアップ。

ほとんどのドキュメントはコード内になければなりませんが、特定のことは、コードの外にドキュメント化する必要があります。設計者がセットアップ用のコードを取捨選択することは時間の浪費です。(ドキュメントが最新のものである場合)ドキュメント化されたセットアップの効率を改善しました。

以下のようなことをドキュメント化してください:

  • 使用しているタグ。
  • レイヤー用途(コリジョン、カリングおよびレイキャスト 本質的には、各レイヤに対応する使用)
  • レイヤーのためのGUIの深さ(各レイヤーに対応する表示)
  • シーンのセットアップ設定。
  • 複雑なプレハブのプレハブ構造。
  • 共通言語設定。
  • ビルドのセットアップ設定。

一般的なコーディング

9.すべてのコードを名前空間(ネームスペース)内に格納する。

これにより、独自のライブラリとサードパーティのコード間のコードの衝突を回避できます。しかし重要なクラスとの衝突を避けるために、名前空間に依存していません 別の名前空間を使用するとしてもクラス名として、"Object"、 "Action" 、 "Event" は使用しないでください。

10. アサーションの使用

アサーションは、コード内の不変量をテストし論理バグをフラッシュするのに役立ちます。アサーションで利用可能なのはUnity.Assertions.Assertのクラスです。これらですべての条件をテストし 条件が満たされない場合、コンソールにエラーメッセージを書き込みます。アサーションをどのように活用できるかを熟知していない場合はリンク先を参照アサーションを使用したプログラミングの利点を(別名文をアサート)

11. 表示されるテキスト以外の文字列は使用しないでください。

特に、オブジェクトやプレハブを識別するために文字列を使用しないでください。例外があります(Unityで名前でしかアクセスできないものがいくつかあります)。このような場合には、そのようなAnimationNamesやAudioModuleNamesなどのファイルの定数としてこれらの文字列を定義します。これらのクラスは管理不能になった場合はあなたのような何かを言うことができるように、入れ子になったクラスを使用しますAnimationNames.Player.Run。

12. Invoke とSendMessageを使用しないでください。

これらのMonoBehaviourのメソッドは名前で他のメソッドを呼び出します。コード内で名前で呼び出す方法は、追跡するのが困難です(あなたは用途を見つけることが出来ません。そしてSendMessageのスコープは範囲が広いため追跡することはより困難です)

コルーチンとC#アクションを使用して独自のInvokeを簡単にロールアウト(運用)することができます:

public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action action, float time) 
{ 
return monoBehaviour.StartCoroutine(InvokeImpl(action, time)); 
} 

private static IEnumerator InvokeImpl(Action action, float time) 
{ 
yield return new WaitForSeconds(time); 

action(); 
}

次に、あなたのmonoBehaviourでこのようにこれを使用することができます:

this.Invoke(ShootEnemy); //ShootEnemy は引数を取らないvoidメソッドです.

独自のベースMonoBehaviourを実装する場合は、独自のInvokeを追加することができます。

別の安全なSendMessageを実装するのはより困難です。そのかわりに、私はたいてい 親オブジェクト、カレントのゲームオブジェクトや子オブジェクトの現在のコンポーネントを取得し 各種のGetComponent 変数を使用して直接呼び出しを実行します。

(編集:誰かがUnityのEvent Systemの一部であるExecuteEventを代わりに提案しました、これまではまだ分かりませんでしたが、それ以上調べる価値はあります)

13. ゲームが実行中に階層を混乱させるオブジェクトの生成をしないでください。

シーンオブジェクトに親を設定すると、ゲームが実行されているときに簡単に見つけられるようになります。 空のゲームオブジェクト、またはシングルトン(この記事の後半参照)を使用して、コードから簡単にアクセスできるようにすることもできます。 このオブジェクトをDynamicObjectsと呼びます。

14. 正規な値としてnullを使用することについては具体的に記述し 可能な限り避けてください。

nullは不正なコードを検出するのに役立ちます。 しかしnullを暗黙的に渡す習慣があれば、間違ったコードがうまく実行されてしまいずっと後までそのバグに気付かないでしょう。 さらに各層がnull変数の上を通過するときにコードの深部に現れることがあります。 私は合法的な値としてnullを使うのをやめようとします。

私の好みのイディオムは、nullチェックを行わずコードが問題のある場所で失敗するようにすることです。 より深いレベルへのインタフェースとして機能するメソッドでは、nullの変数をチェックし失敗する可能性のある他のメソッドに渡す代わりに例外をスローします。

場合によっては、値が正当にnullになる可能性があり、別の方法で処理する必要があります。このような場合、何がいつ そしてなぜnullになるのかを説明するコメントを追加してください。

一般的にインスペクタで設定された値によく使用されます。ユーザーは値を指定できますが、値が指定されていない場合はデフォルト値が使用されます。これを行うより良い方法は、Tの値をラップする、クラスOptional です(これはNullable のようなものです)。特別なプロパティレンダラーを使用してチェックボックスをレンダリングし チェックボックスがオンの場合にのみ値ボックスを表示することができます。

(残念なことに、ジェネリッククラスを直接使用することはできません.Tの特定の値のクラスを拡張する必要があります。)

[Serializable]
public class Optional
{
public bool useCustomValue;
public T value;
}
[Serializable]
public class Optional
{
public bool useCustomValue;
public T value;
}

コードでは、このように記述できます:

health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;

編集:多くの人から構造体を使用する方が良いと指摘されました(ガベージを生成せず、nullにすることはできません)。 ただし 非ジェネリッククラスのベースクラスとしては使用できないため、インスペクタで実際に使用できるフィールドには使用できません。


15. コルーチンの効果的な使用方法を学びましょう

コルーチンは、多くの問題を解決する強力な方法です。 しかしデバッグが難しく誰も理解できない、あなた自信でさえ理解することが出来ません。

知っておくべきこと:

  • コルーチンを並行して実行する方法。。
  • コルーチンを順番に実行する方法。
  • 既存のものから新しくコルーチンを作成する方法。
  • CustomYieldInstructionを使用するcustom coroutines(カスタムコルーチン)を作成する方法。


//これ自身がコルーチンです 
IEnumerator RunInSequence() 
{ 
yield return StartCoroutine(Coroutine1()); 
yield return StartCoroutine(Coroutine2()); 
} 

public void RunInParallel() 
{ 
StartCoroutine(Coroutine1()); 
StartCoroutine(Coroutine1()); 
} 

Coroutine WaitASecond() 
{ 
return new WaitForSeconds(1); 
}


16. インターフェイスを共有するコンポーネントを操作するには、拡張メソッドを使用します。

編集:どうやらGetComponentなどはインターフェイス用に機能し、このチップを冗長にしています)。 特定のインタフェースを実装するコンポーネントを取得したり、そのようなコンポーネントを持つオブジェクトを見つけると便利なことがあります.

次の例では、代わりにこれらの関数のtypeofのジェネリック版を使用しています。ジェネリック版はインターフェイスで動作しますが、typeofをしないでください。以下の方法は、一般的な方法できちんとこれをラップします。

public static TInterface GetInterfaceComponent(this Component thisComponent) 
where TInterface : class 
{ 
return thisComponent.GetComponent(typeof(TInterface)) as TInterface; 
}

 

17. 拡張メソッドの構文をより便利にする。

例えば:

public static class TransformExtensions  
{ 
public static void SetX(this Transform transform, float x) 
{ 
Vector3 newPosition =  
new Vector3(x, transform.position.y, transform.position.z); 

transform.position = newPosition; 
} 
... 
}

 

18. 守備型のGetComponent代替を使用する

場合によって他のクラスのGetComponentを呼び出すときに、RequiredComponentを介してコンポーネントの依存関係を強制することが常に可能であるとは限りません。 そして、たとえあなたがRequiredComponentを使用しているとしても、それがどこにあるかを期待するコンポーネントをどこで取得し そうでない場合にはセットアップエラーをコード内に示すことは有用です。 これを行うには、エラーメッセージを出力する拡張メソッドを使用するか、見つからないときに、以下のようなより有用な例外をスローします:

 

public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour 
{ 
T component = obj.GetComponent(); 

if(component == null) 
{ 
Debug.LogError("Expected to find component of type " 
+ typeof(T) + " but found none", obj); 
} 

return component; 
}

 

19. 同じことをするために異なる慣用イディオムを使用しないでください。

多くの場合、物事を行うために複数の慣習的な方法があります。 そのような場合は、プロジェクト全体で使用するものを1つ選択します。

理由は次のように:

1)いくつかのイディオムはうまく機能しません。 1つのイディオムを使用すると別のイディオムには適さない一方向のデザインになります。

2)同じイディオムを使用すると、チームメンバーは何が起こっているのかを簡単に理解することができます。 構造とコードを理解しやすくし 間違いにくくなります。

イディオム基の例:

  • ステートマシンvsコルーチン。
  • ネストされたプレハブvsリンクプレハブvsgodプレハブ。
  • データ分離戦略。
  • 2Dのゲームで状態ステートのスプライトを使用する方法。
  • プレハブ構造。
  • スポーン戦略。
  • タイプによって参照(「リンク」)配列vsレイヤーvsタグvsネームvs:オブジェクトを検索する方法。
  • オブジェクトをグループ化する方法:レイヤー参照vs配列(「リンク」)vsタグvsネームvsタイプによって。
  • 他のコンポーネントのメソッドを呼び出すための方法。
  • 自己登録vsオブジェクトのグループの検索。
  • 実行順序の制御(Unityのexecution order設定vsyield logic vsAwake / Start and Update / Late Update 更新の信頼vs手動による方法と任意順序アーキテクチャの使用)
  • ゲーム中のマウスでオブジェクト/ポジション/ターゲットを選択する:選択マネージャとローカル自己管理。
  • シーンの変化との間でデータを保持:を通じて  PlayerPrefs新しいシーンがロードされるときに破壊されない、またはオブジェクト。
  • (アディングおよびレイヤリング、ブレンディング)アニメーションの組み合わせの方法。
  • 入力処理(ローカルvs集中管理)

20. 簡単に一時停止するために、あなた自身の時間のクラスを維持してくださいTime.DeltaTimeとTime.TimeSinceLevelLoadをラップすると、一時停止と時間スケールが考慮されます。 それを使用するには調整が必要ですが異なるクロック(インターフェース・アニメーションやゲーム・アニメーションなど)を実行する場合は、作業が非常に簡単になります。

編集:UnityはunscaledTimeとunscaledDeltaTimeをサポートしています。これにより、多くの状況で独自のTimeクラスが冗長化されます。 グローバル時間をスケーリングすると、あなたが望ましくない方法で書き込まなかったコンポーネントに影響を与える場合には、これは依然として有効です。

21. 更新が必要なカスタムクラスは、グローバル静的時間にアクセスすべきではありません。 代わりに、Updateメソッドのパラメータとしてデルタ時間を取る必要があります。これにより、上で説明したように一時停止システムを実装するときや、カスタムクラスの動作を高速化または低速化したいときに、これらのクラスを使用できるようになります。

 

22. WWWを呼び出す一般的な構造を用います。

サーバー通信する多くのゲームでは、一般的に数十のWWWコールを含みます。あなたはUnityの標準のWWWクラスまたはプラグインを使用して定型文として上に薄い層を書くことで恩恵を得ることができます。

私は通常、すなわちCallImplのコルーチンとMakeHandler(GETとPOSTのために、それぞれ)メソッドの呼び出しを定義します。基本的に、スーパーハンドラを構築するために、プロセッサのパーサ、成功と失敗からMakeHandlerメソッドを使用してメソッドを呼び出します。また、それは、スーパーハンドラを呼び出し 完了するまで待って、URLを作成し CallImplのコルーチンを呼び出します。

これは おおよそ次のとおりです。:

public void Call(string call, Func parser, Action onSuccess, Action onFailure)
{
var handler = MakeHandler(parser, onSuccess, onFailure);
StartCoroutine(CallImpl(call, handler));
}
public IEnumerator CallImpl(string call, Func handler)
{
var www = new WWW(call);
yield return www; handler(www);
}
public Func MakeHandler(Func parser, Action onSuccess, Action onFailure)
{
if (NoError(www))
{
var parsedResult = parser(www.text);
onSuccess(parsedResult);
}
else { onFailure("error text");
}
}

 

これには、いくつかの利点があります。

  • 定型コードをたくさん書く必要がなくなります。
  • 特定の処理(UIコンポーネントのロードの表示や特定の一般的なエラーの処理など)を中枢部分で処理できます。

 

23. あなたが大量のテキストを持っている場合は、同じファイル内に配置します。

インスペクタで編集のためにフィールドに入力しないでください。 Unityのエディタを開くことなく変更することが容易になり、特にシーンを保存する必要はありません。

 

24. ローカライズする予定がある場合は、すべての文字列を1つの場所に分けてください。

これを行うには多くの方法があります。 1つの方法は、文字列ごとにパブリックの文字列フィールドを持つTextクラスを定義することです(デフォルトは英語に設定されています)。 他の言語ではこれをサブクラス化して、対応する言語でフィールドを再初期化します。

より洗練されたテクニック(テキストの本文が大きく、言語の数が多い場合に適しています)は、スプレッドシートで読み込まれ、選択された言語に基づいて正しい文字列を選択するロジックを提供します。

 


 

クラス設計

25.inspectable フィールドの実装方法を決定し それを標準にします

フィールドをパブリックにするか、プライベートにして[SerializeField]としてマークします。 後者は「より正確」ですが、あまり便利ではありません(もちろん、Unity自身が普及している方法ではありません)。 どの方法を選んでも、チームの開発者が公開フィールドをどのように解釈するかを知ることができるように標準にしてください。

  • inspectable なフィールドは公開されている場合。このシナリオでは、publicとは、「実行時に変数が設計者によって変更されるのは安全です。コード内に値を設定しないでください」という意味です。
  • inspectable なフィールドはプライベートでマークされたシリアライズ可能なフィールドの場合。 このシナリオでは、publicは「コード内でこの変数を変更することは安全です」(したがって、あまり多くは見られず、MonoBehavioursおよびScriptableObjectsにはパブリックフィールドは存在しないはずです)。

26.コンポーネントの場合、インスペクタで調整しないでください。  

それ以外の場合は、デザイナーが微調整します。 いくつかのまれなケースでは避けられないものがあります(たとえば、エディタスクリプトによってはそれを保持する必要がある場合など)。 その場合、HideInInspector属性を使用してインスペクタで非表示にすることができます。 

27.PropertyDrawersを使用して、フィールドをよりユーザーフレンドリーにする

PropertyDrawersは、インスペクター内のコントロールをカスタマイズするために使用できます。これにより、データの性質に適したコントロールを作成し 特定のセーフガードを適所に置くことができます(変数の範囲の制限など)。 ヘッダー属性を使用してフィールドを整理し ツールチップ属性を使用してデザイナーに余分なドキュメントを提供します。

28. カスタムエディタに比べてPropertyDrawersの使用が好ましい。

PropertyDrawersは、フィールド・タイプごとに実装されるため、実装する作業がはるかに少なくなります。 それらはまた より再利用可能です - 一度型のために実装され、それらはどのクラスでもその型のために使用することができます。 カスタムエディタはMonoBehaviourごとに実装されているため、再利用性と作業性が低下します。

29. 既定ではMonoBehavioursはシールされています。

一般に、UnityのMonoBehavioursはあまり継承されていません:

  • UnityがStartやUpdateのようなメッセージメソッドを呼び出す方法は、サブクラスでこれらのメソッドを扱うのが難しいです。気をつけなければ、間違った箇所が呼び出されるか、基本メソッドを呼び出すことを忘れてしまいます。
  • カスタムエディタを使用する場合は、通常、エディタの継承階層を複製する必要があります。あなたのクラスを拡張したい人は、自分のエディタを用意するか、またはあなたが提供したもので何でもしなければなりません

継承が必要な場合は、それを避けることができれば、Unityメッセージメソッドを提供しないでください。 そうした場合、それらを仮想化しないでください。 必要に応じて、メッセージメソッドメソッドから呼び出される空の仮想関数を定義して、子クラスがオーバーライドして追加の作業を実行することができます。

public class MyBaseClass
{
public sealed void Update()
{
CustomUpdate();
... //このクラスの更新 
   } 
 //このクラスは独自のアップデートを行う前に呼び出されます
//独自の更新コードにフックするオーバーライドします。
   virtual public void CustomUpdate(){};
} 

public class Child : MyBaseClass
{
override public void CustomUpdate()
{
//カスタム操作を実行
   }
}

これは、誤ったコードの上書きからクラスを防ぎ、それでもそれをUnityのメッセージにフックすることができます。私がこのパターンが好きではない一つの理由は、物事の順序が問題になることです。上の例では、クラスが独自の更新を行った直後に子プロセスが直接操作を行いたい場合があります。

30.インターフェイスをゲームロジックから分離する。

インタフェースコンポーネントは、一般にそれらが使用されているゲームについて何も知らないはずです。ビジュアル化するために必要なデータを提供し イベントに登録して、ユーザーがそれらとやり取りするときを見つけます。インターフェイスコンポーネントはゲームロジックをおこなう必要がありません 。それが有効であることを確認するために入力をフィルタリングすることができますが、メインルールの処理は他の場所で行われるべきです。多くのパズルゲームでは、作品はインターフェイスの拡張であり、ルールを含むべきではありません。(例えば、チェスの駒はそれ自体のルール上の動きを計算すべきではないです)

同様に、入力はその入力に作用するロジックから分離する必要があります。 あなたのアクターに動く意図を知らせる入力コントローラーを使用してください。 アクターは実際に移動するかどうかを処理します。

ここでは、ユーザーが選択肢のリストから武器を選択できるようにする、UIコンポーネントの抜粋された例を示します。これらのクラスがゲームについて知っている唯一のものは、武器クラスです(武器は、このコンテナが表示する必要があるデータの有用なソースであるためです)ゲームはコンテナについても何も知りません。それが関係しているすべてはOnWeaponSelectイベントに登録するだけです。

 

public WeaponSelector : MonoBehaviour
{
public event Action OnWeaponSelect {add; remove; }
   // GameManagerは、このイベントのために登録することができます

public void OnInit(List  weapons)
{
foreach(var weapon in weapons)
{ 

var button = ... //Instantiates a child button and add it to the hierarchy           

buttonOnInit(weapon, () => OnSelect(weapon));
//子ボタンは、オプションが表示され、 
//このコンポーネントへのクリックバックを送信
      }
}
public void OnSelect(Weapon weapon)
{
if(OnWepaonSelect != null) OnWeponSelect(weapon);
}
} 

public class WeaponButton : MonoBehaviour
{
private Action<> onClick; 

public void OnInit(Weapon weapon, Action onClick)
{
... //武器からスプライトとテキストを設定


this.onClick = onClick;
} 

public void OnClick() //リンクUI Buttonコンポーネントのクリック時などでこの方法
    {
Assert.IsTrue(onClick != null);  //は起こるべきではありません

onClick();
}
}


31. 個別のコンフィグレーション、状態ステート、ブックキーピングを分離する。

  • 設定変数(コンフィギュレーション)は、プロパティを使ってオブジェクトを定義するためにインスペクタで調整される変数です。 たとえば、maxHealthです。
  • 状態変数(ステート)は、オブジェクトの現在の状態を完全に決定する変数であり、ゲームが保存をサポートしている場合は保存する必要がある変数です。 たとえば、currentHealthです。
  • 簿記変数(ブックキーピング)は、速度、利便性、過渡的な状態に使用されます。 それらは常に状態変数から完全に決定することができます。 たとえば、previousHealthです。

これらのタイプの変数を分離することで、変更可能なもの、保存する必要のあるもの、ネットワークを介して送信/取得する必要があるもの、そしてこれをある程度強化することが容易になります。

 

ここでは、この設定の簡単な例を示します:

public class Player
{
   [Serializable]
   public class PlayerConfigurationData
   {
      public float maxHealth;
   }

   [Serializable]
   public class PlayerStateData
   {
      public float health;
   }

   public PlayerConfigurationData configuration;
   private PlayerState stateData;

   //book keeping
   private float previousHealth;

   public float Health
   {
      public get { return stateData.health; }
      private set { stateData.health = value; }
   }
}

 

32. パブリックインデックス結合配列の使用を避けてください

例えば、武器の配列、弾丸の配列、およびパーティクルの配列を定義しないでください。コードは次のようになります:

public void SelectWeapon(int index) 
{  
currentWeaponIndex = index; 
Player.SwitchWeapon(weapons[currentWeapon]); 
} 

public void Shoot() 
{ 
Fire(bullets[currentWeapon]); 
FireParticles(particles[currentWeapon]); 
}

これはコード内ではあまり問題になりませんが、きちんとインスペクタで設定してください。 いっそのこと3つの変数をカプセル化するクラスを定義し その配列を作成してください

[Serializable] 
public class Weapon 
{ 
public GameObject prefab; 
public ParticleSystem particles; 
public Bullet bullet; 
}

コードはきれいに見えますが、最も重要なことはインスペクタでデータを設定するのは間違いということです。

 

33. 配列以外の構造体には配列を使用しないでください。

例えば、プレイヤーは3種類の攻撃を受ける可能性があります。 それぞれは現在の武器を使用しますが、異なる弾と異なる動作を生成します。

3つの弾丸を配列にダンプして、この種のロジックを使用したくなるかもしれません:

public void FireAttack() 
{ 
/// behaviour 
Fire(bullets[0]); 
} 

public void IceAttack() 
{ 
/// behaviour 
Fire(bullets[1]); 
} 

public void WindAttack() 
{ 
/// behaviour 
Fire(bullets[2]); 
} 
Enums 列挙型はコード内で見栄えを良くすることができます... 
public void WindAttack() 
{ 
/// behaviour 
Fire(bullets[WeaponType.Wind]); 
}

別々の変数を使用して、どの名前を入れるべきかを示す名前を付ける方が良いでしょう。クラスを使ってきれいにします

[Serializable] 
public class Bullets 
{ 
public Bullet fireBullet; 
public Bullet iceBullet; 
public Bullet windBullet; 
}

これは、他に火災、氷、風のデータがないことを前提としています。

 

34. インスペクタで細かくするためのシリアライズ可能なクラスのデータをグループ化する。

いくつかのエンティティには数十の調整機能があるので インスペクタで正しい変数を見つけることは悪夢になることがあります。

作業を簡単にするには、以下の手順に従ってください:

  • 変数のグループに対して別々のクラスを定義します。 それらをパブリックかつシリアライズ可能にします。
  • 主要クラスでは、上記のように定義された各型のパブリック変数を定義します。
  • AwakeまたはStartでこれらの変数を初期化しないでください。 シリアライズ可能なので、Unityはそれを処理します。
  • 定義に値を割り当てることで、以前と同じようにデフォルトを指定することができます。

これにより、インスペクタ内の折りたたみ可能な単位で変数がグループ化され、管理が容易になります。


[Serializable] 
public class MovementProperties //Not a MonoBehaviour! 
{ 
public float movementSpeed; 
public float turnSpeed = 1; //default provided 
} 

public class HealthProperties //Not a MonoBehaviour! 
{ 
public float maxHealth; 
public float regenerationRate; 
} 

public class Player : MonoBehaviour 
{ 
public MovementProperties movementProeprties; 
public HealthPorperties healthProeprties; 
}


                    

 

35. 公開フィールドに使用されていない場合でも、MonoBehaviours Serializableではないクラスをシリアライズ可能にする

これにより、Inspectorがデバッグモードのときにインスペクタのクラスフィールドを表示することができます。 これはネストされたクラス(プライベートまたはパブリック)に対してもです。

36. インスペクタの変更をコードで修正することは避けてください。

インスペクタで調整可能な変数は構成変数であり、ランタイム定数として扱われ、状態変数の2倍ではありません。 このプラクティスに従うことで、コンポーネントの状態を初期状態にリセットするメソッドを簡単に記述でき、変数の動作をより明確にすることができます。

public class Actor : MonoBehaviour 
{ 
public float initialHealth = 100; 

private float currentHealth; 

public void Start() 
{ 
ResetState(); 
}    

private void Respawn() 
{ 
ResetState(); 
}  

private void ResetState() 
{ 
currentHealth = initialHealth; 
} 
}

   

 

パターン

 

パターンは、頻繁に発生する問題を標準的な方法で解決する方法です。 Bob Nystromの「Game Programming Patterns(無料のオンラインで読むことができます)http://gameprogrammingpatterns.com/」は、ゲームプログラミングで発生する問題にどのようにパターンが当てはまるかを見るのに役立つリソースです。 Unity自身は、これらのパターンを多く使用しています。Instantiateはプロトタイプパターンの例です。 MonoBehavioursはテンプレートパターンのバージョンに従い、UIとアニメーションはオブザーバーパターンを使用し 新しいアニメーションエンジンはステートマシンを使用しています。

これらのヒントは、具体的にはUnityとパターンの使用に関連します。

37. 利便性のためにシングルトン(Singleton)パターンを使用してください。

次のクラスは、それを継承するクラスを自動的にシングルトンにします:

public class Singleton : MonoBehaviour where T : MonoBehaviour 
{ 
protected static T instance; 

//Returns the instance of this singleton. 
public static T Instance 
{ 
get 
{ 
if(instance == null) 
{ 
instance = (T) FindObjectOfType(typeof(T)); 

if (instance == null) 
{ 
Debug.LogError("An instance of " + typeof(T) +  
" is needed in the scene, but there is none."); 
} 
} 

return instance; 
} 
} 
}

シングルトンは ParticleManager 、AudioManager または GUIManager。などの管理に有用です。 

(多くのプログラマーは、XManagerという名前のクラスに対して警告を出します。なぜならクラスの名前が不十分であるか、無関係なタスクが多すぎるように設計されているからです。 概ね私はこれに同意します。 しかし すべてのゲームには少数のマネージャーが存在しますし すべてのゲームで同じことをするので、実際にはこれらのクラスは慣用表現です。)

  • (プレーヤーなど)マネージャーではない固有のプレハブのインスタンスのためにシングルトンを使用しないでください。この原則を遵守しないと、継承階層が複雑になり、特定の種類の変更が困難になります。GameManager(または他の適切なGodクラス)でこれらの参照を維持してください。
  • クラス外から頻繁に使用されるパブリック変数やメソッドの静的プロパティとメソッドを定義します。これにより、GameManager.Instance.playerの代わりにGameManager.Playerを記述することができます。

他のヒントで説明したように、シングルトンは、グローバルデータを追跡するシーンロード間で持続するデフォルトのスポーンポイントとオブジェクトを作成する場合にも有用です。

 

38. ステートマシンを使用して、異なるステートで異なる動作を取得したり、ステート遷移でコードを実行したりします

軽量のステートマシンは、複数の状態を有し各状態は、指定または状態の実行操作の有無を入力して、アクションを更新することができます。これは、コードをよりクリーンにしてエラーが起こりにくくします。Updateメソッドに、それが何をするかを変更するifステートメントやswitchステートメント、またはhasShownGameOverMessageなどの変数の有無を記述することで状態マシンからの符号を得ることが出来ます。


public void Update() 
{ 
if(health <= 0) 
{ 
if(!hasShownGameOverMessage)  
{ 
ShowGameOverMessage(); 
hasShownGameOverMessage = true; //Respawning resets this to false 
} 
} 
else 
{ 
HandleInput(); 
}    
}

より多くの状態ステートがある場合 このタイプのコードは非常に乱雑になる可能性があります。 ステートマシンはそれを非常にクリーンにすることができます。

39. UnityEvent型のフィールドを使用して、インスペクタでObserver パターンを設定する

※Observer パターン(オブザーバ・パターン)とは、プログラム内のオブジェクトの状態を観察(observe)するようなプログラムで使われるデザインパターンです。

UnityEventのクラスでは、ボタン上のイベントと同じUIインターフェイスを使用して、インスペクタの4つのパラメータを取る方法にリンクすることができます。これは入力に対処するために特に有用です。

 

40.フィールド値の変化を検出するためにオブザーバーパターンを使用してください。

変数が変更された場合にのみコードを実行する問題は、ゲームで頻繁に発生します。 私たちは汎用クラスでこれを包括的に解決しました。これにより、値が変わるたびにイベントに登録することができます。 ここでは、healthの例での構築の方法です:

/*値の監視*/health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };

これを確認する場所を確認することなく、どこでも変更できます。たとえば次のように。

if(hit) health.Value -= 10;

健康値healthが0を下回ると、Dieメソッドが呼び出されます。 さらなる議論と実装については、この記事ポストを参照してください。

41.プレハブにアクターパターンを使用してください。(これは、「標準」パターンではありません。基本的な考え方は、Kieran Lordのこのプレゼンテーションを参考にしてください)

プレハブの主なコンポーネントはアクターです。 通常はプレハブの「同一性」を提供するコンポーネントであり、上位の1つのコードは多くの場合相互作用します。 アクターは、他のコンポーネント(ヘルパー)を同じオブジェクト(および場合によっては子供)上で使用して作業を行います。

メニューからボタンオブジェクトを1つにまとめると、SpriteおよびButtonコンポーネント(およびTextコンポーネントを持つ子)を持つゲームオブジェクトが作成されます。 この場合、Buttonはアクターコンポーネントです。 同様に、メインカメラは、通常、付属のカメラコンポーネントに加えて、いくつかのコンポーネント(GUIレイヤ、フレアレイヤ、オーディオリスナ)を備えています。 カメラはアクターです。

アクタは、他のコンポーネントが正しく動作するように要求することがあります。 アクタコンポーネントで次の属性を使用すると、プレハブをより強固で便利にすることができます。

  • あなたのアクターが同じゲームオブジェクトに必要とするすべてのコンポーネントを示すには、RequiredComponentを使用します。 (あなたのアクターは、返された値がnullかどうかをチェックする必要はなく、常にGetComponentを安全に呼び出すことができます)。

  • DisallowMultipleComponentを使用して、同じコンポーネントの複数インスタンスをアタッチすることを防ぎます。 したがって、アクターは、複数のコンポーネントが接続されているときのビヘイビアーを心配することなく、常にGetComponentを呼び出すことができます。

  • アクタオブジェクトに子がある場合はSelectionBaseを使用します。 これにより、シーンビューでの選択が容易になります。

 

[RequiredComponent(typeof(HelperComponent))]

[DisallowMultipleComponent]

[SelectionBase]

public class Actor : MonoBehaviour {

...//

}

 

42. ジェネレータを用いたランダムパターンのデータストリーム。(これは標準モードではありませんが、それが非常に有用であることが判明しました)

ジェネレータは乱数ジェネレータと似ています。特定のタイプの新しいアイテムを取得するために呼び出すことができるNextメソッドを持つオブジェクトです。 ジェネレータは、さまざまなパターンやさまざまな種類の乱数を生成するために、その構築中に操作することができそれらは便利です。なぜなら新しい項目を生成するロジックをアイテムの必要な場所とは別にして、コードがはるかにクリーンになるようにするからです。

ここではいくつかの例を示します:

var generator = Generator
   .RamdomUniformInt(500)
   .Select(x => 2*x); //Generates random even numbers between 0 and 998

var generator = Generator
   .RandomUniformInt(1000)
   .Where(n => n % 2 == 0); //Same as above

var generator = Generator
    .Iterate(0, 0, (m, n) => m + n); //Fibonacci numbers

var generator = Generator
   .RandomUniformInt(2)
   .Select(n => 2*n - 1)
   .Aggregate((m, n) => m + n); //Random walk using steps of 1 or -1 one randomly

var generator = Generator
   .Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
   .Where(n >= 0); //A random sequence that increases on average

私たちは、障害物の生成、背景色の変更、プロシージャルな音楽の作成、単語ゲームで単語を作るときに可能性の高い文字列の生成などに使用しています。ジェネレータはまた、コンストラクトを使って一定でない間隔で繰り返しコルーチンを制御するためにうまく機能します:

 

while (true) 
{ 
//Do stuff 

yield return new WaitForSeconds(timeIntervalGenerator.Next()); 
}

ジェネレータの議論についての詳細は、この投稿を参照してください

 

プレハブとScriptableObject参照

43.すべてにプレハブを使用してください。

プレハブ(またはプレハブの一部)ではないシーン内の唯一のゲームオブジェクトは、フォルダーでなければなりません。 一度だけ使用されるユニークなオブジェクトであっても、プレハブでなければなりません。 これにより、シーンを変更する必要のない変更を簡単に行うことができます。

44.プレハブをプレハブにリンクする。 インスタンスをインスタンスにリンクしない

プレハブへのリンクは、プレハブをシーンにドロップするときに維持されます。 インスタンスへのリンクはありません。 可能な限りプレハブにリンクするとシーン設定が減少し シーンを変更する必要性が減ります。

可能な限り、インスタンス間のリンクを自動的に確立します。 インスタンスをリンクする必要がある場合は、プログラムでリンクを確立します。 例えばプレイヤープレハブはゲームマネージャーが起動時にそれをGameManagerに登録することができます。または、GameManagerは、プレイヤープレハブインスタンスが開始時にそれを見つけることができます。

45.あなたが他のスクリプトを追加したい場合。プレハブのルートにメッシュを入れないでください 

メッシュからプレハブを作るときは、最初にメッシュを空のゲームオブジェクトの親とし それをルートにします。 メッシュノードではなく、ルート上にスクリプトを置く。 そうすれば、インスペクタで設定した値を失うことなく、メッシュを別のメッシュに置き換えるほうがずっと容易です。

46.プレハブの代わりに、共有コンフィギュレーションデータにScriptableObjectを使用します。

あなたがこれを行う場合:

  • シーンはより小さくなります
  • 誤って1つのシーン(プレハブインスタンス上)に変更を加えることはできません。

47. レベルのデータにScriptableObjectを使用する。

レベルデータは、多くの場合、XMLまたはJSONに保存されているが、代わりにScriptableObjectを使用することで、いくつか利点があります。

  • ScriptableObjectはエディタで編集することができます。簡単にデータの検証が可能になることで技術面に暗いデザイナーにも親しみやすくなります。また編集をさらに容易にするためにカスタムエディタを使用することができます。
  • データの読み出し/書き込みと解析を心配する必要はありません。
  • 結果のアセットを分割してネストして管理する方が容易であり 大規模な構成ではなく、ビルディングブロックからレベルを作成する方が簡単になります。

48.ScriptableObjectを使用して、インスペクタで動作を設定する。ScriptableObjectは通常、データの設定に関連付けられますが、データとして「メソッド」を使用することもできます

敵のタイプがあり、各敵にスーパーパワーの束があるシナリオを考えてみましょう。 あなたはこれらの通常のクラスを作ってEnemyクラスのリストを持つことができますが、カスタムエディタを使わなければインスペクタ内に別のSuperPowerのリスト(それぞれが独自のプロパティを持つ)を設定することはできません。 しかし これらのSuperPowerアセットを(ScriptableObjectとして実装する)ことができればよいのです。

このように設定:

public class Enemy : MonoBehaviour 
{ 
public SuperPower superPowers; 

public UseRandomPower() 
{ 
superPowers.RandomItem().UsePower(this); 
} 
} 

public class BasePower : ScriptableObject 
{ 
virtual void UsePower(Enemy self) 
{ 
} 
} 

[CreateAssetMenu("BlowFire", "Blow Fire") 
public class BlowFire : SuperPower 
{ 
public strength; 
override public void UsePower(Enemy self) 
{ 
///program blowing fire here 
} 
}

このパターンに従うとき、次の点に注意してください:

  • ScriptableObjectを確実に抽象化することはできません。 代わりに具象基底クラスを使用し 抽象型であるメソッドにNotImplementedExceptionsをスローします。 抽象属性を定義して、抽象クラスである必要があるクラスとメソッドをマークすることもできます。。
  • ジェネリックであるScriptableObjectはシリアライズできません。 ただし ジェネリックベースクラスを使用して、すべてのジェネリックを指定するサブクラスをシリアライズするだけで済みます。

49.ScriptableObjectを使用してプレハブを特殊化します。

2つのオブジェクトの構成が一部のプロパティでのみ異なる場合は、2つのインスタンスをシーンに配置し インスタンスのプロパティを調整するのが一般的です。 通常は、2つのタイプの間で異なるプロパティの別のクラスを別々のScriptableObjectクラスにする方が優れています。

これにより、柔軟性が向上します:

  • 特殊化クラスの継承を使用して、異なるタイプのオブジェクトにさらに固有のプロパティを与えることができます。
  • シーンの設定ははるかに安全です(単に目的のタイプのオブジェクトを作るためにすべてのプロパティを調整する代わりに、ScriptableObjectを選択するだけです)
  • コードの実行時にこれらのオブジェクトを操作する方が簡単です。
  • あなたは2タイプの複数のインスタンスがある場合は、変更を行うときはそのプロパティは常に一貫性があることを知っています。
  • コンフィギュレーション変数のセットを混在および一致させることができるセットに分割できます。

以下はセットアップの簡単な例です:

[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
public float maxHealth;
public float resotrationRate;
} 

public class Actor : MonoBehaviour
{
public HealthProperties healthProperties;
}

特殊化の数が多い場合は、特殊化を通常のクラスとして定義し それを保持できる適切な場所(GameManagerなど)にリンクされたScriptableObjectでこれらのリストを使用することができます。 それを安全に、より速く便利にするために必要な融合方法がもう少しあります。

最小限の例を以下に示します:

public enum ActorType 
{ 
Vampire, Wherewolf 
} 

[Serializable] 
public class HealthProperties 
{ 
public ActorType type; 
public float maxHealth; 
public float resotrationRate; 
} 

[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")] 
public class ActorSpecialization : ScriptableObject 
{ 
public List healthProperties; 

public this[ActorType] 
{ 
get { return healthProperties.First(p => p.type == type); } //Unsafe version! 
} 
} 

public class GameManager : Singleton   
{ 
public ActorSpecialization actorSpecialization; 

... 
} 

public class Actor : MonoBehaviour 
{ 
public ActorType type; 
public float health; 

//Example usage 
public Regenerate() 
{ 
health  
+= GameManager.Instance.actorSpecialization[type].resotrationRate; 
} 
}

 

50. CreateAssetMenuを使用することで属性アトリビュートは自動的にScriptableObjectが作成したメニューが作成/アセットに追加されます。


 

デバッギング

51. 効果的なUnityのデバッグ機能を使用する方法を学びます。

  • Debug.Logステートメントにコンテキストオブジェクトを追加して、それらの生成場所を確認します。
  • Debug.Breakを使用してエディタでゲームを一時停止します(たとえば、エラー状態が発生し そのフレームのコンポーネントプロパティを調べたい場合など)。
  • ビジュアルデバッグのためにDebug.DrawRay関数とDebug.DrawLine関数を使用します(たとえば、レイキャストがヒットしなかった理由をデバッグする場合、DrawRayは非常に便利です)。
  • ビジュアルデバッグには、Gizmosを使用します。 また、DrawGizmo属性を使用してモノ動作以外のギズモレンダリングを提供することもできます。
  • デバッグインスペクタビューを使用して(インスペクタを使用してUnityで実行時にプライベートフィールドの値を確認してください)。

52. 効果的にIDEのデバッガを使用する方法を学びます。

Visual StudioでのUnityゲームのデバッグ。を参照してください。

53. 時間の経過とともに値をグラフに描画するビジュアルデバッガを使用する

これは物理学、アニメーション、その他の動的プロセス、特に散発的な障害をデバッグするのに非常に役立ちます。 グラフのゆらぎを見ることができ、同時にどの他の変数が変化するかを見ることができます。 目視検査では、あまりにも頻繁に変化する値や明白な原因なしにドリフトするような特定の種類の奇妙な行動も明確になります。 私たちはモニターコンポーネントを使用しますが、それにはいくつかの方法があります。

54. 改良されたコンソールログを使用します。

カテゴリに応じた出力の色分けを可能にするエディタ拡張を使用し それらのカテゴリに従って出力をフィルタリングすることができます。私たちは Editor Console Proを使用していますが、いくつかが利用可能です

55. Unityのテストツールを使用して、特にアルゴリズムと数学的コードをテストする。

チュートリアルを参照するか またはUnit testing at the speed of light with Unity Test Tools.の投稿、またUnity Test Tools を参照してください。

56. Unityのテストツールを使用して、「スクラッチパッド」のテストを実行します。

Unityのテストツールは、正式なテストには適していません。 シーンを実行することなくエディタで実行できる便利なスクラッチパッドテストにも利用できます。

57.スクリーンショットを撮影するためのショートカットを実装します。

多くのバグは視覚的であり、スナップショットを撮ることができるときに報告する方がずっと簡単です。 理想的なシステムでは、連続するスクリーンショットが上書きされないように、PlayerPrefsにカウンタを保持する必要があります。 スクリーンショットは、プロジェクトフォルダ外に保存して、誤ってリポジトリにコミットすることを避ける必要があります。

58. 重要な変数のスナップショットを印刷するためのショートカットを実装します。 

これにより、ゲーム中に何か予期せぬ事態が発生したときに、情報を簡単に記録することができます。 もちろん、どの変数がゲームに依存しているか。 あなたはゲームで発生する典型的なバグに導かれます。 例は、プレイヤーと敵の位置、またはAIアクター(「それを追いかけようとしているパス」などの「思考状態」)です。

59.簡単にテストさせるためのデバッグオプションを実装します。 

いくつかの例:

  • すべての項目のロックを解除します。
  • 敵を無効にします。
  • GUIを無効にします。
  • プレイヤーを無敵にします。
  • すべてのゲームプレイを無効にします。

誤ってデバッグオプションをコミットしないように注意してください。デバッグオプションを変更することでチーム内の他の開発者を混乱させる可能性があります。

60. デバッグのショートカットキーの定数を定義し 一か所に保存します。

デバッグ・キーは、ゲーム入力の残りの部分と同じように単一の場所で通常(または都合よく)処理されません。 ショートカットキーの衝突を回避するには、中枢の場所に定数を定義します。 代わりに、デバッグ機能であるかどうかにかかわらず、すべてのキーを1か所で処理することもできます。 (欠点は、このクラスがこれのためだけにオブジェクトへの余分な参照を必要とするかもしれないということです)。

61.プロシージャルなメッシュ生成を行う際に頂点に小さな球を描画するか生成します。

メッシュを表示するために三角形やUVを使い始める前に、頂点が正しい位置にあることを確認し メッシュが正しいサイズであることを確認するのに役立ちます。

 


 

パフォーマンス

62. パフォーマンス上の理由におけるデザインと設計に関する一般的なアドバイスには細心の注意を払ってください。

  • このようなアドバイスはしばしば作り話に基づいており、テストによって裏打ちされていません。
  • 時にはアドバイスは、テストに裏打ちされているが、テスト自体が破綻しています。
  • 時にはアドバイスは正しいテストに裏打ちされているが、それらは非現実的または異なる状況にあります。(例えば、それを使用して配列はジェネリックリストよりも高速であるかを示すのは簡単です。しかし 実際のゲームの文脈でこの差はほとんど常に無視できる程度です。同様に、テストはあなたのターゲットデバイスとは異なるハードウェアに適用されていた場合、その結果はあなたにとっては意味がありません。)
  • 時にはアドバイスは懸命ですが、古くなっている事があります。
  • 時には、アドバイスが適用されますが、トレードオフが存在します。スローゲームその船は時々ない高速のものよりも優れています。そして、頻繁に最適化されたゲームはシッピング(発送)を遅らせる厄介ななコードが含まれている可能性が高くなります。

パフォーマンスのアドバイスは、以下で概説するプロセスを使用して、実際の問題の原因を迅速に追跡するのに役立つことを覚えておくと便利です。

63. 初期の段階からターゲットデバイス上で定期的にテストをします。

デバイスは、非常に異なるパフォーマンス特性を持っていますが それを驚かないでください。早期から問題について知っていることでより効果的に対処することができます。

64.パフォーマンスの問題の原因を追跡するために効果的にプロファイラを使用する方法を学びます。

65. 必要に応じてより正確なプロファイリングを行うには、カスタムプロファイラを使用します。

Unityのプロファイラでは、何が起きているのかを明確に把握することはできません。プロファイルフレームが不足したり、プロファイリングが深すぎるとテストが意味をなさないほどゲームが遅くなることがあります。 私たちはこれに独自のカスタムプロファイラーを使用していますが、アセットストアで代替案を見つけることができるはずです。

66. パフォーマンス向上の影響を測定する

パフォーマンスを向上させるために変更を加えるときは、その変更が実際に改善されていることを確認するために測定します。 変更が測定可能または過失でない場合は、元に戻します。

67. より良いパフォーマンスのために読みにくいコードを書かないでください。

次のいずれかの状況がない限り:

  • プロファイラを使用して問題を抱えているソースを特定し変更後のパフォーマンスを測定しパフォーマンスの向上が保守性の損失に比べて十分に高い場合。

OR

  • あなたが自分は何をしているか解っている場合。

 


 

基本ネーミングおよびディレクトリ構造

68. ネーミングは、慣例やフォルダ構造に従います

一貫した命名およびフォルダ構造は、対象を見つけやすく、対象が何であるかを把握しやすくなります。

おそらく、独自の命名規則とフォルダ構造を作成したいと思うでしょう。 ここでは一例を示します。

命名の一般原則

  1. それが何であるかを示すような呼び方をします。鳥は鳥と呼ばれるべきです。
  2. 発音したり記憶したりできる名前を選択します。 あなたがマヤのゲームを製作したとして レベルをQuetzalcoatisReturn(ケツアルコアトルの復活)と名付けてはいけません。
  3. 一貫して。あなたが名前を選択すると、それに固執。別の内の1つの場所とbuttonContainerで何かbuttonHolderを呼び出さないでください。
  4. ComplicatedVerySpecificObject:このように、パスカルケースを使用してください。スペース、アンダースコア、またはハイフンを使用しないでください (例外的に同じものの異なる性質を命名を参照してください)。
  5. 進捗状況(WIP、 final)を示すために、バージョン番号、または単語を使用しないでください。
  6. 略語は使用しないでください:DVamp@ WはDarkVampire@Walkでなければなりません。
  7. 設計ドキュメント内の用語を使用してください:ドキュメントがdieのアニメーションを呼び出した場合、DarkVampire@DeathではなくDarkVampire@Dieとします。
  8. 最も具体的な記述子は、VampireDarkではなくDarkVampireのままにしておきます。 PauseButtonではなく、ButtonPausedです。 たとえば、すべてのボタンが「Button」という単語で始まらない場合、インスペクターで一時停止ボタンを見つける方が簡単です。 [多くの人がそれを好む傾向があります。なぜならグループ分けを視覚的により明白にするからです。 名前はグループ化するためのものではなく、フォルダもあります。 名前は同じタイプのオブジェクトを区別して、信頼性が高く高速に配置することができます。
  9. いくつかの名前は、配列を形成します。PathNode0、PathNode1、例えば、これらの名前の数字を使用してください。常に0ではなく1から開始します。
  10. シーケンスを形成しないもののために数字を使用しないでください。例えば、Bird0、Bird1、Bird2はフラミンゴ、イーグル、ツバメにする必要があります。
  11. 二重下線__Player_Backupとプレフィックス一時オブジェクト。

 

同じものを異なる性質(特徴)から命名

基本の名前をアンダーバーで区切って「性質(特徴)」を記述します。

例えば:

  • GUIボタンステート  EnterButton_Active、EnterButton_Inactive
  • テクスチャ  DarkVampire_Diffuse、DarkVampire_Normalmap
  • スカイ  JungleSky_Top、JungleSky_North
  • LODグループ  DarkVampire_LOD0、DarkVampire_LOD1

例えばRock_Small、Rock_LargeはSmallRock、LargeRockにする必要があり、アイテムの異なるタイプを区別するだけにこの規則を使用しないでください。

 


 

構造

シーン、プロジェクトフォルダ、およびスクリプトフォルダの構成は同様の形式に従ってください。ここでは、取り掛かりにいくつかの要約例を示します。

 

フォルダー構成


MyGame
   Helper
      Design 
      Scratchpad
   Materials
   Meshes
      Actors
         DarkVampire
         LightVampire
         ...
      Structures
         Buildings
         ...
      Props
         Plants
         ...
      ...
   Resources
      Actors
      Items
      ...
   Prefabs
      Actors
      Items
      ...
   Scenes
      Menus
      Levels
   Scripts
   Tests
   Textures
      UI
      Effects
      ...
   UI 
MyLibray
   ...
Plugins
SomeOtherAsset1
SomeOtherAsset2
...
                   

 

シーンの構造

Main 
Main
Debug
Managers 
Cameras
Lights
UI
   Canvas
      HUD
      PauseMenu
      ...
World
   Ground
   Props
   Structures
   ...
Gameplay
   Actors
   Items
   ...
Dynamic Objects

 

スクリプトのフォルダ構造

Debug 
Debug
Gameplay
   Actors
   Items
   ...
Framework
Graphics
UI
...

 

 

ヒープメモリやらメモリ管理で検索が多いので記事にしてみたのですが 久しぶりのブログ更新なので後半部分をまとめたところで力尽きました。力尽きました

メインは海外サイトの翻訳なのですが どうにもわかりにくい。メモリ管理で検索するんだからわりと初級?だもの この説明では難しいことでしょう

後日改めて、 もう少しわかりやすくまとめた前半部分を追記しますのでこのページにリンクを貼らないで置いてください。

<文字列制限があるので記事を分割するとアドレスが変わる可能性があるのです>

うちのブログだと 8割がたゲームメーカーからのアクセスなのであんまり初心者むけ記事は見てもらえないような気もするんですけどね... (´・ω・`)

 

 ヒープメモリとは?                                              

更新予定

■とりあえずはここらへんを読んでおいてください

  • Unity - マニュアル- 自動メモリ管理を理解する
  • 【Unity】Unite 2015「Unity パフォーマンス・チューニング」レポート ? Qiita

     

     

     不必要なヒープ割り当ての一般的な原因               

    この元ブログ記事では以下の様な内容でメモリ管理について論じています

    1. メモリ管理とメモリリークの一般的な原因
    2. ユニティプロファイラによるメモリリークの検出
    3. C#でのオブジェクトプーリング

    ここから元記事の意訳

    • この最初の投稿は.NETとMonoのガベージコレクトされた世界のメモリ管理の基本について議論します。またメモリリークのいくつかの一般的な原因について説明します。
    •  
      2番目の投稿はメモリリークを発見するためのツールについてです。Unityプロファイラは、この目的にとって素晴らしいツールですが、それはまた高価です。そこであなたが唯一の無料ツールでメモリリークを発見する方法をお見せするために、.NETの逆アセンブラと共通中間言語(CIL)について議論します。
    •  
      3番目の投稿は、ふたたびC#のオブジェクトプーリングを議論します。ここでも焦点はUnity/ C#の開発で生じる特定のニーズです。

     

    ■ガベージコレクションの限界

    最近のほとんどのオペレーティング・システムでは、スタックとヒープに動的メモリを分割します。多くのCPUアーキテクチャ(PC/Mac そしてスマートホン/タブレットが含まれます)でこのような命令セットがサポートされています。c#では値の型を区別することでこれらをサポートしています(単純な組み込み型などとして宣言されているユーザー定義型  enum  または  struct)と参照型(クラス、インタフェース、およびデリゲート) 値型はスタックに参照型はヒープに割当てられます。スタックは新しいスレッドの開始時に設定された固定サイズを有しています。これは通常は小さくて たとえばwindowsのデフォルトの.Netスレッドは1Mb。このメモリはスレッドの主な機能とローカル変数をロードし メイン関数から呼び出される関数(それらのローカル変数)をアンロードするために使用されます。その一部は動作を高速化するためにCPUのキャッシュにマッピングすることができます。コールが深すぎないかローカル変数は巨大すぎないかといったスタックオーバーフローを恐れる心配はありません。スタックのこの使われ方は構造化プログラミングの概念と整合することを確認してください。

     

    オブジェクトがスタックに対して大きすぎる場合またそれらの機能がスタックに長く残ってしまった時にヒープが発生します。ヒープはそれ以外のメモリの箇所で 通常はOSとプログラムの規則の要求通りに増加します。一方スタックにある場合はメモリ管理をするのは簡単で(フリーなメモリの場所を記憶するためにはポインタが使用されます) ヒープを管理することは,ヒープが断片化した場合すぐに割当てられたオブジェクトからオブジェクトを開放させる要求をださなければならないことは明らかで,すべてのスイスチーズの穴のような場所を覚えることに気を止めなければならず 全く楽しくありません。そこで自動メモリ管理を導入します。 自動割当てのタスクは 主にあなたにとってのすべてのチーズの穴のようなメモリ断片を追跡する簡単なもので実質すべてのプログラミング言語でサポートされています。 自動割当てをする決定の多くの困難なタスクを特にあなたが心配する必要はなくオブジェクトは割当て解除の準備ができています。

    この後者のタスクは、ガベージコレクション(GC)と呼ばれています。あなたのランタイム環境を伝える代わりに 実行時に一定の間隔でオブジェクトへのすべての参照を追跡し確定後オブジェクトが破壊されてメモリが解放されたときに おそらくコードからオブジェクトに到達できなくなります。ガベージコレクトは未だに学者によって活発に研究されていることが ガベージコレクトのアーキテクチャが。Netフレームわーっクから大幅に変更と改善をされている理由です。またUnityはMONOの最新バージョン(2.11 / 3.0)にデフォルト設定はありませんが、その代わりに バージョン2.6を使用しています(正確には、私のWindowsの2.6.5は、Unityの4.2.2 がインストールされています。Unty4.3についても同様です) あなたが自身でこれらを確認する方法に不明な点があるようであれば 次の投稿でそれを説明していきます。

     

    バージョン2.6以降のMONOに導入されたGC関連は主要な改変のひとつです。新しいバージョンがGenerational GC(世代別GC)を使用しているのに対し バージョン2.6ではまだあまり洗練されていない ベームガベージコレクタを使用しています。最近の世代別GCはゲームなどのリアルタイムアプリケーションのために(制限内ですが)良好に動作します。ベームスタイルのGCは一方で比較的まれな間隔(通常は一回あたりのフレームのゲームに比べてはるかに少ない頻度)でヒープ上のガベージごみを徹底的な検索を実行して動作します。これによって一定の間隔でフレームレートの低下が引き起こされる圧倒的な傾向があり プレーヤーは迷惑を蒙(こうむ)ります。UnityのドキュメントではFPSが低下する状態の場合にはSystem.GC.Collect()を呼ぶことを推奨しています(例えば、新しいレベルのロードやメニューを表示する時) しかし多くのゲームの種類においてこのような機会はまれにしか発生せず あなたがGCのをしたい時にガベージコレクトを拒否するかもしれません。あなたの唯一の選択肢は勇敢にメモリを自分で管理することで、それについて残りの2つの記事で述べています。

     

    ■メモリマネージャを適切なものに

    ユニティ/ .NETの世界では「メモリを自分で管理する」ことを意味するかについて明確にしてみましょう。メモリ割当てがされる方法に影響を与えるためのあなたの力は(幸いなことに)非常に限られています。あなたは クラス(常にヒープ上に割り当てられた)または  構造体(クラス内に含まれていない限りスタック上に割り当てられます)のカスタムデータ構造を選択し採用することができます。あなたがより多くの魔法の力が必要な場合は、C#のUnsafe コードのキーワードを使用する必要があります。しかしUnsafe コードは、UnityのWeb Player及びおそらく他のいくつかのターゲットプラットフォームで実行されないことを意味している検証できないコードです。このことと  その他の理由により安全ではありませんので使用しないでください。

    ※Unite EUROPE2015のKEYNOTEを観る限りではUnsafeコードは普通に使用されているようですが?

    そのためスタックの上記の制限およびC#の配列がためだけの糖衣構文であるためのSystem.Array(これらはクラスです)自動ヒープ割当てを避けることはできません。不要なヒープの割り当てを何によって避けるべきかをこの記事の次の(そして最後の)セクションで知ってください。

    メモリ割当て解除されるまではあなたの及ぶ力は限定的です。実際ヒープされたオブジェクトの割り当てを解除することができる唯一の方法はGC(ガベージコレクト)であり その働きはあなたから遮蔽されています。ヒープ上のオブジェクトのいずれかへの最後の参照がスコープ外になったときにGCがその前にそれらに触れることができないため、何らかの影響を与えることはできます。定期的なガベージコレクション(あなたが抑制することができない)が解放するものがないときには非常に高速になる傾向があるため この制限された力は、たいへん実用的な関連性を持っていることが判明しました。この事実は様々なアプローチの基礎となるオブジェクトプーリングについて3番目の投稿で議論します。

     

     

    ■foreachループを避けるべきでしょうか?

    foreachループを使用する代わりにforまたはwhileループを使用するべき理由は、foreachは実際には単なるシンタックスシュガー(糖衣構文:読み書きのしやすさのために導入される構文)で、以下のようなコンパイラの前処理コードだからです。

    foreach (SomeType s in someList)
        s.DoSomething();

    ...次のように:

    using (SomeType.Enumerator enumerator = this.someList.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            SomeType s = (SomeType)enumerator.Current;
            s.DoSomething();
        }
    }

    foreach はenumeratorオブジェクトを生成します。 System.Collections.IEnumeratorインターフェースのインスタンス シーンの裏側で このオブジェクトはスタックとヒープのどちらに生成されるのでしょうか?両方が実際に可能であるため これは大変よい質問です。もっとも重要なことはSystem.Collections.Generic の namespace (List, Dictionary, LinkedList, etc.)は構造体を返すのに GetEnumerator()メソッドは大変適しています(参考記事が書かれた時点ではUnityはMono 2.6.5を使用)

     

    foreachのループを回避する必要がありますか?Unityでコンパイル可能なC#のコードでそれらを使用しないでください。foreachを標準のgeneric collections (List etc.) を用いて反復処理するために、あなたがC#のコードをコンパイルに最近のコンパイラを使用していますか?Visual Studioのほかに無料の.NET Framework SDKが適正であることを前提とし(ただし、確認はしていない)Monoと最新バージョンに付属しているMonoDevelopも同様に適正とします。外部コンパイラを使用して foreachループを反復するための他の種類のコレクションをどうすればいいでしょうか? 残念ながら答えはありません。 foreachを安全に使用するためどのコレクションを使用するかあなた自身が見つけ出すためのテクニックの使用法を次回のブログポスト元記事2で論じてみます。

     

    ※2

  • http://www.gamasutra.com/blogs/WendelinReich/20131119/203842/C_Memory_Management_for_Unity_Developers_part_2_of_3.php
  • http://monodevelop.com/

     

    ■あなたはclosuresやLINQを避けるべきでしょうか?

    C#では匿名メソッドやラムダ式(かなり互いに同一)が提供されていてdelegateキーワードと=>演算子 を用いてそれらを作成することが出来ます。これらはしばしば便利なツールであり List<T>.Sort()) や LINQ といったライブラリ関数を使用するときたいへん重宝します。 匿名メソッドとラムダ式は、メモリリークを起こしているでしょうか? それは依存しています C#コンパイラには、実際にそれらを処理する2つの非常に異なった方法があります。違いを理解するために次のコードを考慮してください。

     

    int result = 0;
        
    void Update()
    {
        for (int i = 0; i < 100; i++)
        {
            System.Func<int, int> myFunc = (p) => p * p;
            result += myFunc(i);
        }
    }

     このようにスニペットmyFuncというdelegateを1フレームあたり100回生成する計算を実行します。しかしながら Monoはヒープ割当てを最初にUpdate()関数(※筆者の環境で52Byte)が呼ばれた時だけ実行します。サブシーケンスでヒープ割当ては実行されません。どうなっているのでしょう?コードリフレクタ(part2を参照)を使用すると  C#コンパイラはmyFunc型の静的フィールドでSystem.Func  型、int型 >を含むクラスでUpdate()関数を単に置き換えることがわかります。 このフィールドには明示的な名前 f__am $ cache1を取得しています  (システム上で多少異なる場合があります)。つまりdelegatorは一度だけ割り当てられて、その後キャッシュされてるのです。

    今度は、デリゲートの定義にマイナーチェンジを作ってみましょう:

    System.Func<int, int> myFunc = (p) => p * i++;

     

    closuresは関数型プログラミングの柱です。closuresはデータに関数を結びつけます、より正確には関数外で定義される非ローカル変数に結びつけます。 このmyFuncの例の場合'p'はローカル変数ですが'i'はUpdate()関数に属する非ローカル変数です。 C#コンパイラはmyFuncをアクセスと非ローカル変数を更新することができるように変換する必要があります。 リファレンス環境にあわせて新規にクラスを宣言することでmyFuncの生成を実現しています。 このクラスのオブジェクトは、for-loopを通過するたびに割当てられるため(※筆者の環境ではフレームごとに2.6kb) 突然巨大なメモリリークに襲われます。

    もちろん closuresや他の言語機能がC#3.0で導入された主な理由はLINQです。closuresがメモリーリークに繋がることができればゲームでLINQを使用しても大丈夫でしょうか?LINQの一部はiOSなどの実行時コンパイラをサポートしないOS上で明らかに動作しません。メモリ管理の側面からLINQはとにかく良くありません。

  • http://answers.unity3d.com/questions/376884/using-linq-when-building-to-ios.html
  • クロージャ ? Wikipedia

     

    次の信じられないような基本的な表現:

    int[] array = { 1, 2, 3, 6, 7, 8 };
    
    void Update()
    {
        IEnumerable<int> elements = from element in array
                        orderby element descending
                        where element > 2
                        select element;
        ...
    }

    毎フレーム68バイトを割当て( Enumerable.OrderByDescending() が28バイト、 Enumerable.Where()で40バイト)!

    ここでの犯人はclosuresではありません 拡張メソッド IEnumerable:LINQは最終的な結果を出力するために 中間の配列を作成する必要があり そのあとそれらをリサイクルするシステム領域を持ちません。(※私はLINQの専門家ではないので リアルタイムの実行環境で安全にそれらのコンポーネントを使用する方法はわかりません)

    ■コルーチン

    あなたがStartCoroutine()経由でコルーチンを起動した場合は、暗黙的にunityのインスタンスコルーチンのクラス(私のシステムで21バイト)とEnumerator(16バイト)の両方を割り当てます。yield's や resumesコルーチンは何も割当てをしません そこでメモリリークを回避するためにはゲームが実行されている間StartCoroutine()の呼び出しを制限してください。

    void Update()
    {
        string string1 = "Two";
        string string2 = "One" + string1 + "Three";
    }

    ■ストリングス

    C#とunityのメモリに関しては文字列に言及しない訳にはいきません。文字列は変わっていて、ヒープ割当てでありながらイミュータブルです。2つの文字列を連結するとき(これらの変数や文字列定数で)ランタイムは 結果を含む少なくとも1つの新しい文字列オブジェクトを割り当てることがあります。String.Concat()これはFastAllocateString()と呼ばれる外部メソッドにより効率的に行われます。(ただし上記の例ではシステム上の40バイト)。 変更または実行時に文字列を連結する必要がある場合にはSystem.Text.StringBuilderを使用します。

    オブジェクト指向プログラミングにおいて、イミュータブル(immutable)なオブジェクトとは、作成後にその状態を変えることのできないオブジェクト のこと

    ■ボクシング

    時々データがスタックとヒープを移動する必要があります。例えば 次のように文字列のフォーマットを設定する場合です:


    string result = string.Format("{0} = {1}", 5, 5.0f);

    次のシグネチャを持つメソッドを呼び出しています。
    public static string Format(
    	string format,
    	params Object[] args
    )

    言い換えると、整数「5」および浮動小数点数「5.0f」はSystem.ObjectがFormat()を呼び出した際にキャストする必要があります。 しかし、オブジェクトが参照型なのに対して他の2つは値型です。 C#はこのようにヒープ上の割り当てられたメモリに値をコピーすることがあり、Format()はint型とfloat型のオブジェクト参照を新しく作成します。 このプロセスがボクシング、 反対の動作をアンボクシングと言います。 このstring.Format()のビヘイビアは問題となるかもしれません すなわちヒープメモリからとにかくそれを取り除く必要があります(新しいstring のために) しかしボクシングは期待される場所に出現するわけではありません。悪名高い例として 等価演算子"=="を実装するとき (例えば、複雑な数を表す構造体)

    ※このような場合のボクシングを避ける方法についての記事をリンク先で読むことが出来ます。

  • http://stackoverflow.com/questions/10390782/why-cant-we-override-equals-in-a-value-type-without-boxing

     

    ■ライブラリメソッド

  • Dictionary<K, V>.KeyCollection と Dictionary.ValueCollectionはクラスで構造体ではありません。 これらはつまり"foreach (K key in myDict.Keys)..."に16バイト割りつけるということです。
  •  List.Reverse() は標準のインプレース配列反転アルゴリズムを使用しています、それがヒープメモリを割り当てないと考えるかと思いますが、少なくともMono 2.6バージョンでは間違いです。 ここで拡張メソッドは.NET/Mono バージョンで最適化されないときに代用出来るかもしれません。少なくともList.Reverse()を使用するのと同じやり方で、ヒープ割当てを避けるための管理がなされます。

    public static class ListExtensions
    {
        public static void Reverse_NoHeapAlloc<T>(this List<T> list)
        {
            int count = list.Count;
    
            for (int i = 0; i < count / 2; i++)
            {
                T tmp = list[i];
                list[i] = list[count - i - 1];
                list[count - i - 1] = tmp;
            }
        }
    }

     

     

    ここから、これまでの考察をまとめた別記事の意訳です

     ■Unity、C#と.NET /モノラルでメモリ使用量を削減             

  • Reducing Memory Usage in Unity, C# and .NET-Mono - Andrew Fray

     

    iOSの上のUnityはMonoのヒープマネージャの初期のバージョンを使用しています。このマネージャは、パッキングを行いませんので、ヒープメモリが断片化された場合は新しいメモリを確保してしまいます。私はUnity開発者がこの問題を回避するために新しいヒープマネージャで作業している印象なのですが、今のところメモリリークを持つゲームは増え続けるメモリを消費してしまいます。C#は,すばやく読みやすさを犠牲にすることなく強力なコードを記述することができる楽しい言語です。しかしこの方法の欠点は自然なC#のコードを書くことがガベージコレクションの多くを生成してしまうことです。これを回避する唯一の方法はヒープ割当てを排除または低減することです。私は機能性を低下させることなく これを行うための便利な方法をリストにまとめました。最終的な効果として あなたのC#コードは,はるかにC ++のように見えC#の力の一部を失ってしまうものの それにより生きてきます副次効果としてヒープアロケーションは本質的にスタックアロケーションよりもよりCPU集約型なので、おそらく同様にいくつかのフレーム時間を節約できます。あなたの努力目標のためにユニティプロファイラは多くのメモリ割当てを作る作業を助けてくれます。情報は多くはないですが。プロファイラを開きゲームを実行し、CPUプロファイラを選択して、問題箇所をソートするためのGCアロケータカラムをタップします。はじめにそれらの機能にこれらのガイドラインを適用してください。

     

    foreach()を使用しないでください。

  • GetEnumerator()を呼び出しlist型にヒープ上のenumeratorを割り当てるのをすぐに止めてください。for(;;)構文に,より詳細なC ++スタイルを使用することが必要になります: あなたが配列上にforeach-ingしなければ,それは何もメモリを割当てません。私はそれがfor(...){}のための構文糖衣(読み書きのしやすさのために導入される構文)になった特殊なケースだと推察します。

    Ian Horswill氏このポイントを指摘していただき有り難うございます。

  • Ian Horswill http://www.cs.northwestern.edu/~ian/
     

     

    strings文字列を避けてください。

     

    文字列は.NETでは不変でヒープメモリ領域に割り当てられています。UIのためC言語の様にその場で それらを操作することはできません StringBuilders で、できるだけ文字列への変換を遅らせてメモリ効率の良い方法で文字列を構築します。あなたはリテラルがメモリ内の同じインスタンスを指す必要があるためキーとしてそれらを使用することができますが、あまりにも多くの操作はしないでください。

  •  http://msdn.microsoft.com/en-us/library/2839d5h5(v=vs.71).aspx
  •  C#入門-リテラル - WisdomSoft

    構造体を使用してください。

  • MonoのStruct型はスタック上に割り当てられています、ユーティリティクラスを持っていれば構造体作成にスコープを残すことはありません。任意のコピーのコストを回避するためにrefとパラメータの前に付ける必要があります。構造体は値で渡されることを覚えておいてください。

     

    スコープ結合を固定サイズ配列の構造体に交換してください。

     

    固定サイズの配列を持っている場合スコープを残さずメンバ配列を再利用できます。またはそれをミラーフィールドを持つ構造体を作成することができます メンバー配列に置き換えることのいずれかを検討してください。 私は4つのフィールドを持っているスプラインクラスのControlList構造体を呼び出すときはいつもVector3 [4]に置き換えます。そしてインデックス基準のアクセスのためにthis[]プロパティを追加しました。それは頻繁に呼び出される関数だったので大量のメモリ割当てを回避できました。

     

    リストを受け渡す際refキーワードを使用した参照渡しが好ましいです。

    あなたはヒープメモリに渡すリストを必要とするのが正解で、これで何も変わらないように聞こえますか? ですがこれにより必要に応じて最適化を可能にします。

  •  パラメーターの引き渡し (C# プログラミング ガイド)
  • 方法 - メソッドに構造体を渡すこととクラス参照を渡すことの違いを理解する (C# プログラミング ガイド)

     

    ■頻繁な呼び出しをする関数をメンバ変数としてをローカルストレージにスコープすることを検討してください。 

    毎回大きなリストを呼びださなければならない時,リストにメンバ変数が作成されストレージはフレーム間持続します。 次のフレームにはメモリ領域が不足するのでC#のリストでバッファ領域から削除するためClear()を呼びださなければいけません。それによりコードが酷く読みにくくなるので、わかりやすいコメントが必要になりますが 作業は大幅に軽減できます。

  • 2-2 変数と定数 https://msdn.microsoft.com/ja-jp/library/cc406734.aspx
  • 変数のスコープ http://wisdom.sakura.ne.jp/programming/cs/cs23.html

     

     

    ■IEnumerable拡張メソッドを避けてください。

    ほとんどの便利なLINQのIEnumerable拡張メソッドは、新しい割り当てを作成することは言うまでもありません。私は.Any()が IList<>で呼びだされた時  Count> 0で Virtual関数がメモリ割当てを引き起こす First() とLast()のようなIlist()関数のメソッドも同様だと考えたのですが予想に反していました。この結果とforeach()の制限から あなたのインタフェースにはIEnumerable <>抽象化を避けて代わりにIList <>を使用するべきでしょう。

    ■関数ポインタ使用を最小限に 

    クラスメソッドをデリゲートやFunc<>に代入するとボックス化の原因となりメモリ割当てを引き起こします。私にはボクシングのないメソッドへのリンクを格納するための任意の方法はわかりません。デカップリングに大きな恩恵があるため割当てられたメモリは残り続けますが関数ポインタのほとんどは残してきましたが,いくつかは削除しました。

    ※デカップリング(Decoupling) プログラミングとデザインで、一般的に 再利用され可能な限り少数の依存関係を使用してコードを作成する行為のこと

     

    クローン化されたマテリアルに注意してください。 

    任意のレンダラのマテリアルプロパティを取得する場合、それに何も設定しない場合でもマテリアルを複製します。このマテリアルはGC'dされていません レベルの変更(シーンロード)またはResources.UnloadUnusedAssetsを呼び出したときのいずれかの場合のみクリーンアップされます。マテリアルを調整する必要はありませんが知っていればmyRenderer.sharedMaterialを使用してください。

    ※MaterialPropertyBlock を使用することで改善できる場合があります

  •  

    ディクショナリキーとして生の(未加工)構造体を使用しないでください 

    Dictionary型を使用する時 あなたのキー値が構造体である場合はTryGetValue()またはインデックスアクセサを使用してDictionaryから値を取得することが出来ますが ここでメモリ割当てが発生します。これを回避するには構造体のためにIEquatable を実装します。Dictionaryはいくつかの既定の等値比較をするたびに作成されていると思います。

     ライアン・ウィリアムズhttps://twitter.com/quxmore のおかげで,これを見つけられました。

    Enum.GetValues()またはmyEnumValue.ToString()を使用し過ぎないようにしてください。

    Enum.GetValues(typeof(MyEnum))は呼びだされるたびに配列を割り当てます。Enum.GetNames()も同様です。他の場所同様にUIを扱う際enum変数に.ToStringを加えて使用すべきです。enum値をループなどcachedEnumNames [(int)myEnumValue]に使用するように簡単にこれら両方の配列をキャッシュすることができます。enum型の値を手動で設定している場合 例えばフラグですが、これは動作しません。

     

     

     

     

     

     C# Coroutine WaitForSeconds Garbage Collection tip - Unity Community 

     

  • C# Coroutine WaitForSeconds Garbage Collection tip - Unity Community

     

    追記:メモリ最適化の実装例としてWaitForSecondsコルーチンを最適化してガベージコレクトを抑える記事を簡単にまとめておきます。

     

    詳細はリンク先の記事で確認してください

     

     

     

    A: <Before>"yield return new WaitForSeconds()" は呼び出されるごとにガベージメモリを21バイト消費します

    ("yield return null"なら9バイトのメモリ消費)

      そこで"new WaitForSeconds()" を使用前にキャシュしてヒープ上に確保するメモリを削減してみます。

    これにより一回のIEnumeratorメソッドの呼び出しを9バイトに減らすことが出来ました。

    B:Yieldメソッドを場合分けして辞書登録し Yieldersクラスとして呼び出します。WaitForSeconds()メソッドは直値を指定すると A:の例では”0.1f、0.5f"のフロート型の数値ですが、呼び出されるたびにヒープメモリには新規に数値がキャッシュされ これが9バイトのメモリ消費となってしまいます。

    そこでUnityのシステムが管理する値 WaitForFixedUpdate()に代入される値をstatic型で置き換えることで、値がヒープメモリに複製されないようにしています。

    A:も ガベージコレクト対策としては悪いアイデアではないのでケースに応じて使い分けることが良いかもしれません。

    IEnumeratorメソッドはゲームオブジェクトのスクリプトに含まれることが多いためアタッチされたインスタンスが数百単位で複製される場合もあり 多くのヒープメモリを消費してしまいます。そのためガベージコレクト回避の対策として"Yield〜WaitForSeconds()" まわりを最適化することは有効だと考えられそうです。

     

     

     

    A:

    IEnumerator myAwesomeCoroutine()

    {

        while (true)

        {

            doAwesomeStuff();

            yield return new WaitForSeconds(waitTime);

        }

    }

    WaitForSeconds shortWait = new WaitForSeconds(0.1f);

    WaitForSeconds longWait = new WaitForSeconds(5.0f);

     

    IEnumerator myEvenAwesomerCoroutine()

    {

        while (true)

        {

            if (iNeedToDoStuffFast)

            {

                doAwesomeStuffReallyFast();

                yield return shortWait;

            }

            else{

                dontDoMuch();

                yield return longWait;

            }

        }

    }

     

    B:

    using UnityEngine;

    using System.Collections;

    using System.Collections.Generic;

     

    public static class Yielders {

     

        static Dictionary<float, WaitForSeconds> _timeInterval = new Dictionary<float, WaitForSeconds>(100);

     

        static WaitForEndOfFrame _endOfFrame = new WaitForEndOfFrame();

        public static WaitForEndOfFrame EndOfFrame {

            get{ return _endOfFrame;}

        }

     

        static WaitForFixedUpdate _fixedUpdate = new WaitForFixedUpdate();

        public static WaitForFixedUpdate FixedUpdate{

            get{ return _fixedUpdate; }

        }

     

        public static WaitForSeconds Get(float seconds){

            if(!_timeInterval.ContainsKey(seconds))

                _timeInterval.Add(seconds, new WaitForSeconds(seconds));

            return _timeInterval[seconds];

        }

      

    }

    <呼び出し側>

    必要な時間間隔にするにはYieldメソッドを複数回記述する。

    yield return Yielders.Get(this.updateinterval);

     

     

    【追記】

    『テラシュールブログ』さんでUNITYのパフォーマンス最適化について、まとめられているそうですので興味のある方は参照してください。

    もう一か所、自分の説明不足のところをフォローしていただいたようなので、こちらの記事も参照してみてください。

     

    ■ 更新ついでに近況 … 仕事が一段楽したので近々ブログ復帰予定です.  (10/29)

     

    ■リファレンス

  • Gamasutra- Wendelin Reich's Blog - C# Memory Management for Unity Developers (part 1 of 3)
  • Gamasutra- Wendelin Reich's Blog - C# Memory Management for Unity Developers (part 2 of 3)
  • Gamasutra- Wendelin Reich's Blog - C# Memory Management for Unity Developers (part 3 of 3)
  • Three kinds of generic object pools to avoid memory deallocations in Unity-based games. Released
  • Reducing Memory Usage in Unity, C# and .NET-Mono - Andrew Fray
  • https://unitygem.wordpress.com/memory-management/
  • https://web.archive.org/web/20140624150214/http://msdn.microsoft.com/en-us/library/e59b22c5(v=vs.71).aspx
  • https://web.archive.org/web/20140702030338/http://unitygems.com/wp-content/uploads/2012/12/Stack2.png
  • C#-.NETがやっていること 第二版
  • C#の高速化入門
  • メモリ管理の話_エスキュービズム勉強会20140926
  • ↑このページのトップヘ