Unity3d

2013年12月06日

うにばな (Graphics.DrawMesh で高速な描画?)

 

 

drawMeshTest

■webプレイヤー

https://dl.dropboxusercontent.com/u/18978109/DrawMeshTest/DrawMeshTest.html

 

Unity公式のフォーラムで大量のメッシュを描画する方法として有効だというはなしのGraphics.DrawMesh命令を簡単に解説します

Unityの新しいバージョンでプロパティブロックまわりが改良されたそうなので プロパティブロックを使用してサンプルを作成してみました

< どこが新規に変わったのかいまいちわからないのですが >

 

ちなみに情報としては古めなので すでにほかのところに詳しい記事があるかもしれませんが、ここの管理人は怠けものなので国内の記事はあまり見ていません。







■Graphics.DrawMesh

● static function DrawMesh(mesh: Mesh, position: Vector3, rotation: Quaternion, material: Material, layer: int, camera: Camera = null, submeshIndex: int = 0, properties: MaterialPropertyBlock = null): void;

● static function DrawMesh(mesh: Mesh, matrix: Matrix4x4, material: Material, layer: int, camera: Camera = null, submeshIndex: int = 0, properties: MaterialPropertyBlock = null): void;

 

【パラメータ】

mesh描画するメッシュデータ

positionメッシュの位置。

rotation      メッシュの回転。

matrixメッシュの変換行列(位置、回転、その他の変換)。

materialマテリアルを指定する。

layerレイヤを使用する。

camera    :デフォルトはNULL。メッシュはすべてのカメラで描画されますが 指定がある場合指定されたカメラでレンダリングします。

submeshIndex描画するメッシュのどのサブセット。これは、複数の材料で構成されたメッシュに適用される。

propertiesメッシュが描画される直前にマテリアルにプロパティ変更を適用します。※MaterialPropertyBlock

 

DrawMesh命令は1フレームごとにメッシュを描画します。通常のゲームオブジェクトと同様にシェーディングされ影が落ちてプロジェクターの影響を受けることができます。デフォルト状態ではすべてのカメラにレンダリングされますが特定のカメラを指定することもできます。

ゲームオブジェクトの作成と管理のオーバーヘッドを避けて大量のメッシュを描画したい場合DrawMesh命令は有効に機能します。DrawMeshはすぐにメッシュを描画せず Updateの後にレンダリングパイプラインにデータを送り、そのあと通常のレンダリングプロセスの一部としてレンダリングされます。 すぐにメッシュを描画したい場合は、Graphics.DrawMeshNowを使用します。

DrawMeshはすぐにメッシュの描画がされないため、関数の呼び出しの時に材料特性を変更すると、メッシュはそれらをピックアップすることはありません。あなたは、同じマテリアルを用いてメッシュのシリーズを描きたいのですが、すこしだけマテリアルのプロパティ(例えば色を変更)をする場合は、MaterialPropertyBlockのパラメータを使用します。

 





■MaterialPropertyBlock

MaterialPropertyBlockGraphics.DrawMeshによって使用されているRenderer.SetPropertyBlockです。、同じマテリアルで複数のオブジェクトを描画するような場合、たとえばメッシュの色を変更したい場合シェーダ内のプロパティの値を部分的に変更することで対応します。

 

【機能】

AddColor            : カラープロパティを追加します。

AddFloat             :フロート型プロパティを追加します。

AddMatrix         : マトリックス型プロパティを追加します。

AddTexture       :テクスチャ型プロパティを追加します。

AddVector         :ベクター型プロパティを追加します。

Clear                  : プロパティ値のクリア。

GetFloat             : プロパティブロックからフロートを取得します。

GetMatrix          : プロパティブロックから行列を取得します。

GetTexture        : プロパティブロックからテクスチャを取得します。

GetVector          : プロパティブロックからベクトルを取得します。

 

 

【Example】

public class  XXXXX : MonoBehaviour
{

     private MaterialPropertyBlock _PropertyBlock; 
     private int _ColorPropertyId ;

 

void Start()
{

               _PropertyBlock    = new MaterialPropertyBlock();
               _ColorPropertyId = Shader.PropertyToID("_Color");

}

void Update() 
{

               _PropertyBlock.Clear(); 
               _PropertyBlock.AddColor(_ColorPropertyId, Color.white);    
            
  Graphics.DrawMesh( Mesh, Position , Rotation, Material, 0, MainCamera, 0,_PropertyBlock);

  }

}

 

PropertyBlock を効率よく使用するためには1つのブロックを作成してDrawMeshをコールするたびにブロックを再利用する方法です。

Graphics.DrawMesh()の前に使用ブロックをクリアし AddXXX を使用して値を追加します。

指定できるプロパティは使用しているシェーダ内で登録されているプロパティとなります。 複数のシェーダを使用する場合はShader.Find(シェーダ名)で書き換え対象のシェーダを指定する必要があります。

 

 

●DrawMeshManager.cs

using System.Collections.Generic;
using UnityEngine;
using Assets;

public class DrawMeshManager : MonoBehaviour
{
   
    public Mesh ObjMesh;
    public Material Material1;
   
    public Camera MainCamera;
    public Transform Player;
    public float ForceDrawWithinDistance = 50.0f;
   
    public int Maxval = 100;
    public float  Scaler = 2.0f;   
   
    private List Objs = new List();
   
    private MaterialPropertyBlock _PropertyBlock;
    private int _ColorPropertyId ;
   
   
    private void Start()
    {
       
        MainCamera = Camera.main;                
        float Offset=(Maxval*Scaler/2.0f)-Maxval*Scaler;
        int ObjectType = 0;
       
        for(var x=0 ; x        {                
            ObjectType = x%2;          
            for(var y=0 ; y            {                   
                for(var z=0 ; z                {    
                                                                
                    ObjData newObj = new ObjData( ObjectType,
                                            new Vector3((float)x*Scaler+Offset,(float)y*Scaler+Offset,(float)z*Scaler+Offset),
                                            Quaternion.Euler(0, 0 ,0));

                    Objs.Add(newObj);                                       
        
                }
           }
           
        }
       
        _PropertyBlock = new MaterialPropertyBlock();
        _ColorPropertyId = Shader.PropertyToID("_Color");       
        
    }

   
    private void Update()
    {
       

        Vector3 playerPosition = Player.position;
        var FrustumPlanes = GeometryUtility.CalculateFrustumPlanes(MainCamera);

        
        float _timer = Time.timeSinceLevelLoad;               
        var ObjBounds = ObjMesh.bounds;
       
        foreach (var Obj in Objs)
        {
                        
        //    int ObjType   = Obj.ObjType; // メッシュオブジェクトを複数定義する場合に使用する
            
                ObjBounds.center = Obj.WorldPosition;

       
            float distance = Vector3.Distance(Obj.WorldPosition, playerPosition);

       
            if (distance < ForceDrawWithinDistance || GeometryUtility.TestPlanesAABB(FrustumPlanes, ObjBounds))
            {
                   
                float ofst = Mathf.Sin((Obj.WorldPosition.y + _timer)/4.0f)*1.5f;
                                
                _PropertyBlock.Clear();
                _PropertyBlock.AddColor(_ColorPropertyId, Obj.Color);   
                Graphics.DrawMesh(ObjMesh, Obj.WorldPosition + new Vector3(ofst,0,ofst) , Obj.Rotation, Material1, 0, MainCamera, 0,_PropertyBlock);
                                          
                    
            }
        }
    }
}

●ObjData.cs 【構造体クラス】

using System;
using UnityEngine;


namespace Assets
{
    public class ObjData
    {
        private  int _ObjType;
        private  Vector3 _WorldPosition;
        private  Quaternion _Rotation;
        private  Color _Color;
       

        public ObjData(int ObjType, Vector3 worldPosition, Quaternion rotation)
        {
            _ObjType = ObjType;
            _WorldPosition = worldPosition;
            _Rotation = rotation;
           
            if (ObjType == 0)
            {
               
                _Color = Color.white;
               
            }
            else
            {

               _Color = new Color(1.0f,0.2f,0.05f);
               
            }      
            
        }

        public Vector3 WorldPosition
        {
            get { return _WorldPosition; }
        }
        public Quaternion Rotation
        {
            get { return _Rotation; }
        }
        public int ObjType
        {
            get { return _ObjType; }
        }       
        public Color Color
        {
            get { return _Color; }
        }
       
    }
}

いままでは配列で管理して結合した後GPUに転送という方法が知られていましたが それにに比べると メモリ消費も少なくて なにより頭を悩ませることがないのがいいですね。

画像のStatisticsで表示されるDrawCall数は3000を越えていますが FPS値はふつうに高いままなので Unityの中身はわからないので想像ですが、おそらくCPU側でGraphics.Draw関数をコールした回数だと思われます。VRAMへの描画は1フレームで行われているらしいです。

左のGUIのFPS表示はエディタから実行した数値です。 PCの環境にもよりますがwebプレイヤーでは60フレーム程度はでていると思います。

Unityのツリーシステムなどでは メモリ使用量はモデルの頂点数分の合計となるためかなり大きめな数値となっています おそらくは内部でメッシュをマージして転送する方法を行っているのだと思いますが、今回のDrawMesh関数を使用するサンプルはアップデート内部でシェーダにモデル1つ分のデータを送るだけなのでGPU側のメモリはほとんど消費されていません

それからスキンモデルにもDrawMwshを使用してみたいところなのですが現時点ではスキンウェイトのモデルはダイナミックバッチがかからない仕様になっているそうなのです

スキニングのフィルターがブラックボックスなので調べてみないとわかりませんが、アニメーションするキャラクターを大量に配置したい場合はスキンウェイト用のシェーダを自作する必要があるかもしれません。



akinow at 09:46|PermalinkComments(0)TrackBack(0) Clip to Evernote

2013年11月28日

うにばな(ノーマル(法線)基本 と ノーマルマップ合成)

 

 

こんばんは 予告通り 「三つ目時 」=テレビ東京標準時の木曜夜7時半の更新です。

だれもわからないだろうけどw

 

今回はノーマル法線とノーマルマップの合成に関してのはなしです。

 

sample_1sample_2

sample_3image022

●たっきゅんのガ☆チンコ開発日記 http://takkun.nyamuuuu.net/blog2/archives/1627

● ISO-IEC 18026 - Clause 8, Spatial reference frames

のほうから画像だけお借りしました 不都合があればお申し出ください

--------------------------------------------------------------------------------------------------------------------------------------------------------

 

ノーマルマップはオブジェクトの法線ベクトルをテクスチャ画像のカラー値に置き換えたものです。

接空間 (Tangent Space) ポリゴン面の法線に対して垂直に接する面の座標空間です。グーローシェーディングがかかっている場合はポリゴン面上の法線はポリゴンを構成する頂点の法線がグラデーションで補完された値になります。

図のようにノーマル法線方向をZ軸 ポリゴンのUV空間 U方向をX軸 V方向をY軸と見立てます。

ノーマルマップは X軸成分をRカラー値、Y軸成分をGカラー値に置き換えて ポリゴン上のポイントではテクスチャのRカラー値によって法線はTangent方向へ傾くように計算され Gカラー値によってBinormal方向に傾くように計算されます。 それによってポイントあたりの法線方向が歪められてライト計算に反映されると凹凸が表現できるという仕組みです。

テクスチャカラーのRGB成分は整数値の0~255または0~1で定義されていますので 0.5を中心としてそれより小さければマイナスの値 大きければプラスの値とみなして扱います。

 

Unityのシェーダで接空間 (Tangent Space)データを使用する場合はシェーダー入力の構造体にappdata_tanを指定します。 これで取得できるのはモデルのAssetデータの変換時に計算されたタンジェント値です。

struct appdata_tan {
    float4 vertex : POSITION;
    float4 tangent : TANGENT
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
}; 

もちろんappdata_fullですべてのデータを取得しても構いませんがデータはなるべく使用するものだけにしたほうが軽量化出来ますので そこら辺はケースバイケースで判断してください。

binormalは以下の式で計算出来ます。

float3 binormal = cross( IN.normal, IN.tangent.xyz ) * IN.tangent.w;

あまりにざっくりなので解説しますと 2つのベクトルに対して外積(cross)をとることで直行するベクトルを算出することができます。(INはシェーダinput構造体を表します)

上の式ではnormalとtangentをcross(外積)とることでbinomal(副法線)を算出しています。ベクトルのw成分は

vector(ベクトルの方向(x,y,z)、ベクトルの大きさw)で定義されるベクトルの大きさ成分wをかけています。

cross(外積)を用いた2つのベクトルに直行するベクトルを求める式は たとえばポリゴンを自前で生成した時に

法線を求める用途などにも使われます。 あとポリゴン面の表裏判定などですかね。

 

これが法線計算の基本ですが これを理解していると例えば 波のようにポリゴンをうねらせたりといったモデルを

変形させた時にシェーダー内部でノーマルを計算させたり 自前でポリゴン生成をするときに法線の計算をしたりという応用ができます。

 

●TANGENTの計算式

float3 tangent;

float3 c1 = cross(Normal, float3(0.0, 0.0, 1.0));
float3 c2 = cross(Normal, float3(0.0, 1.0, 0.0));

if (length(c1)>length(c2))
{
    tangent = c1;
}
else
{
    tangent = c2;
}

tangent = normalize(tangent);

●BINORMALの計算式

float3 binormal;

binormal = cross(Normal, tangent);
binormal = normalize(binormal);

 

補足>ノーマルベクトルをタンジェント空間に投影するコードが以下のようになっています ノーマルを再計算させたい時に参考にしてみてください

ノーマルベクトルとポリゴンのタンジェント空間座標(UVZ)の内積をとって角度変換し0~1の範囲に丸めているだけです。


float3 normalTS;

normalTS.x = dot(normal, Input.binormal);
normalTS.y = dot(normal, Input.tangent);
normalTS.z = dot(normal, Input.normal);

normalTS = normalize(normalTS);

return float4(normalTS * 0.5 + 0.5, 1);

 

< ここまでが基本の解説でした>

 

NormalMap - Polycount Wiki     http://wiki.polycount.com/NormalMap#TSNM

これを読めば大丈夫じゃないかというのが PolyCount Wiki のノーマルマップの解説です 英語ですけど解説図が多いのでほぼ眺めるだけでも内容は理解できると思います。

このサイトを紹介すればよかっただけのような気もしますね・・・

 

 

 ■今回のお題 ノーマルマップの合成で質感のクオリティアップを図ります。      

 

●メリットとしては

モデル全体を覆うノーマルマップとデティール用のノーマルマップそれぞれが あまり大きなテクスチャを使用しなくてもモデルのデティールアップが図れることが挙げられます。たとえば壁などで細かい質感を出すために高解像度テクスチャを使用しなくてもわりとラフなテクスチャでも十分見栄えのする質感表現が可能になります。

ノーマルマップ自体は多少重た目なのですが、シェーダ内でLOD命令を使用してカメラからの距離に応じてシェーダの式を切り替えることで軽量化できるかと思います。 ビルドインシェーダーのウォーターシェーダ を参考にしてみてください。

 

 

■オーバーレイを使用する合成の例
オーバーレイの式を利用したノーマルマップの合成ですがオーバーレイはカラーの数値が
0.5を境に合成式を変化させます。 以前パーティクルのアイデア1で加算と乗算を重ねあわせましたが 
それとほぼ同じです。たとえば背景に対してブレンド式を適用すればカラー部分を強調したままの
加算が再現できます。
 

 BlendOverlayf(base, blend)     (base < 0.5 ? (2.0 * base * blend) : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend)))

   
      float4 norm   = tex2D(_BumpMap, IN.uv_BumpMap);
      float4 norm2 = tex2D(_BumpMap2, IN.uv_BumpMap2);
      dest = norm2<0.5 ? 2*norm*norm2 : 1-2*(1-norm)*(1-norm2);
      dest = lerp(norm2, dest, _Opacity);
      o.Normal = UnpackNormal(dest);

 

例) フォトショップのオーバーレイモードを使用してノーマルマップを調整することができます

normalmapfilter

NVIDIA のフォトショップ用ノーマルマップフィルタでノーマルマップを生成した場合すこし薄めのノーマルマップが仕上がると思います。 この場合フォトショップでレイヤーを使用してノーマルマップの深さをコントロールすることが出来て

下図のようにオーバーレイモードで同じノーマルマップをレイヤーに複製して重ねます。レイヤーのオペーシティ(濃度)をコントロールしてノーマルの深さをコントロールします。

もちろん部分的にレイヤー切り貼りで重ねれば すこしコントラストを強めたい部分だけに作用します。

overlay

 

■ノーマライズを使用する合成の例   

図はUDK(アンリアルエディターのシェーダーツリー)のノーマルマップをブレンドするサンプルです

NormBlend1

内容は2つのマップのノーマル成分を加算してノーマライズするだけという単純なものですが オーバーレイ計算の式では条件判定に三項演算子を使用していました 以前解説したようにシェーダーはif条件式が苦手なのでできれば避けたいところです。ノーマライズ版のほうが多少軽めかもしれません。ただノーマライズもそんなに軽い関数ではないので状況によって実装して比べてみるしか無いですかね。

 
   
float4 norm   = tex2D(_BumpMap, IN.uv_BumpMap);
     float4 norm2 = tex2D(_BumpMap2, IN.uv_BumpMap2);
                o.Normal  = normalize(float3(norm.xy + norm2 .xy, norm.z));

 

補足> normalize, dot, inversesqrt などのオペレータはUnityが最適なコードに変換するので自作のものを使用しないこと。 pow, exp, log, cos, sin, tan などの計算関数は非常に重たいのでなるべくテクスチャ参照(例えばカラーカーブをテクスチャで用意したもの)などを利用することがあげられています。

●    Unity - Optimizing Graphics Performance

 

その他の方法については以下のリンクに詳しい情報が掲載されていますので参照してみてください

Blending in Detail - Self Shadow http://blog.selfshadow.com/publications/blending-in-detail/

 

シェーダを実装した画像

DetailMap 

 

■ 『 Diffuse Detail Bump.shader 』

Shader "custom/Diffuse Detail Bump"{

Properties {

_Color ("Main Color", Color) = (1,1,1,1)
_MainTex ("Base (RGB)", 2D) = "white" {}
_DetailMap ("Detail (RGB)", 2D) = "gray" {}
_BumpMap ("Normalmap", 2D) = "bump" {}
_DetailBumpMap ("Normalmap(Detail)", 2D) = "bump" {}
_DetailScale("DetailScale", Range (0.01, 1)) = 0.4

}

SubShader {

Tags { "RenderType"="Opaque" }
LOD 250

CGPROGRAM

#pragma surface surf Lambert

#include “UnityCG.cginc”

sampler2D _MainTex;
sampler2D _DetailMap;
sampler2D _BumpMap;
sampler2D _DetailBumpMap;
fixed4 _Color;
half _DetailScale;



 struct Input { float2 uv_MainTex;
float2 uv_DetailMap;
float2 uv_DetailBumpMap;
                       };

void surf (Input IN, inout SurfaceOutput o)
{
// デティールマップ(カラー)をテクスチャカラーと合成する
fixed4  c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
            c.rgb *= tex2D(_DetailMap,IN.uv_DetailMap).rgb*2;

// 2つのノーマルマップから法線ベクトルをフェッチして合成する
fixed4 Normal1 = tex2D(_BumpMap, IN.uv_MainTex);
fixed4 Normal2 = tex2D( _DetailBumpMap ,IN.uv_DetailBumpMap) * float4 (_DetailScale,_DetailScale,0,0);

//アウトプットノーマルに法線ベクトルをセットする
            o.Normal = UnpackNormal (Normal1+Normal2);

// アウトプットカラーをセットする
            o.Albedo = c.rgb; 
            o.Alpha   = c.a;
}
ENDCG
}
Fallback "Diffuse"
}


 fixed4   Normal2 = tex2D( _DetailBumpMap ,IN.uv_DetailBumpMap) * float4 (_DetailScale,_DetailScale,0,0);

画像の例ではこの式の部分を以下の例のように書き換えてあります

例)

#include “UnityCG.cginc”

float4 _DetailBumpMap_ST;

……………….

float2    UVCoord = IN.uv_MainTex * _DetailBumpMap_ST.xy

             Normal2 = tex2D( _DetailBumpMap , UVCoord) * float4 (_DetailScale,_DetailScale,0,0);

 

UVがメインテクスチャと同じものを使用する場合はテクスチャスロットのoffsetの数値を使用してこのように
記述します。 
”テクスチャ名+_ST”のUnityマクロの使用法は以前の”パーティクルのアイデア2”の記事を参照してください。 
 

Tips>

ノーマルマップは基本的にRとGのチャンネルしか使用しないためBやAlphaチャンネルは空いた状態ですので、テクスチャの枚数を圧縮したい場合はこの空きチャンネルを利用することが出来ます。 一般にはAO(アンビエントオクルージョンマップ)やマスクテクスチャなどをいれるようです。 スマホなどのテクスチャ枚数が処理速度に影響が出やすいものはなるべくあいているチャンネルを利用してテクスチャ枚数を減らすことでメモリ効率がよくなります。

 

 

記事が長くなりすぎましたので ノーマルマップ生成方法やツールの使用法などの解説はまた後日にします。 

その気になれば10年後も可能とい(ry

もうすこし内容をつめようと思ってたんですけどねー すこし中途半端な感じになってしまいました次回への課題にします。

 

ではまた



akinow at 19:36|PermalinkComments(0)TrackBack(0) Clip to Evernote

2013年10月04日

うにばな(ノイズベースパーティクル1)

 

今回はCurlノイズを使用したパーティクルの制御についてのおはなしです。

 

CurlノイズはPerlinノイズをベースにしたランダムノイズ関数で 適用することでパーティクルをあたかも流体計算をしているように振る舞わせることができます

Perlinノイズは空間座標を与えるとだいたい同じような値を返してくる特徴があって これが空間の物理特性を表現するのに都合がよく 考え方は空間にポテンシャル場 (流体のムラ)が存在しそれによって、移動するパーティクルの速度変化が影響を受けるというシュミレーションです。

そういえば先日 光子の速度に影響するヒッグス粒子が発見されましたね イメージはあんなふう

 

流体といえばナビエ・ストークス方程式(Navier-Stokes)をもとにしたシュミレーションを耳にしたことがあるかと思います。3DCGでセルグリッドの流体シュミレーションなどはだいたいナビエ・ストークス式をもとにした近似方程式で計算されています。 ナビエ・ストークス方程式は非常に複雑で まだ解を求めることが出来ないため。3DCG計算の分野では用途に合わせて簡易方程式を利用して計算を行うことで計算量を減らしています。

現在の流体計算はシュミレーション上はそれっぽく振る舞うので実用的にはこんなもんでいいかな というところで

おさまっているようです。

sqex75

スクウェア・エニックス オープンカンファレンスレポート(前編) - GAME Watch

2年ほどまえのスライドからですが、ゲームで使用する場合グリッドベースでまじめに計算を行う場合は計算量に見合ったクオリティは得られないため それっぽく振る舞うノイズ関数を使用した流体計算のほうがコストに見合うということが語られています。 FFアグニなどもこの方法みたいですね DeepDownもそれっぽいですが

 

 

■今回の実行サンプル

CurlTest1

● WebPlayer CurlTest.html

● UnityPaclage CurlTest(131002).unitypackage

今回の場合はノイズを毎回計算してパーティクルの挙動に反映させるやりかたを考えます。参考にしたのがこのサイト

■curl noise for particles

curl noise for particles - syphobia -- the procedural way

 

パーティクルシステムで乱流をシュミレートする場合フレームごとに弱い力を速度に与える方法がとられます。(fig1

Unityのパーティクルシステムのアセット 例えばXeffectなどはソースも公開されていますので解析してみるとRandomで数値を加算しているだけです。

fig1:

particle[i].force.x+=random(-0.1,0.1);

particle[i].force.y+=random(-0.1,0.1);

 

ベクトルは−1から1で全ての角度方向をカバーできますのでこれでパーティクルの方向はコントロールできます。

そこにPerlinノイズによるゆらぎを加味することでそれらしくパーティクルの速度をコントロールします。

fig2:

particle[i].force.x+=perlin(particle[i].x*scale, particle[i].y*scale,1.352+time);

particle[i].force.y+=perlin(particle[i].x*scale, particle[i].y*scale,12.814+time);

 

パーティクルの座標をスケーリングしてPerlinノイズで(-1.0,1.0)の範囲で値を導きますz座標 にはある定数値と時間を与えてノイズフィールドを変更します。

という感じです これは自分の知ってるカールノイズなのか?という疑問もあるのですが それっぽければ良いので 今回はこの記事を信用しましょう。

 

 

■コードの解説

●"ParticleBufferAccess_Curl.cs"

using UnityEngine;
using System.Collections;


public class ParticleBufferAccess_Curl : MonoBehaviour {
       
public Vector3 Amount = new Vector3(1.0f,1.0f,1.0f);
const int bufferSize =  1000;
   
private ParticleSystem.Particle[] particles = new ParticleSystem.Particle[bufferSize];
private float[] dx =new float[bufferSize];
private float[] dy =new float[bufferSize];
private float[] dz =new float[bufferSize];
   
private Perlin noise;
   
public float  scale = 0.1f;
public float  speed = 0.05f;
      
void  Start ()
{
   
        noise = new Perlin ();
             
}    
       
void LateUpdate() {

int length = particleSystem.GetParticles(particles);
int i = 0;
       

while (i < length) {


    float  scalex = Time.time  * speed + 0.1365143f;
    float  scaley = Time.time  * speed +   1.21688f;   
    float  scalez = Time.time  * speed +    2.5564f;   
       
    dx[i] = noise.Noise(particles[i].position.x*scale, particles[i].position.y*scale, particles[i].position.z*scalex);
    dy[i] = noise.Noise(particles[i].position.x*scale, particles[i].position.y*scale, particles[i].position.z*scaley);
    dz[i] = noise.Noise(particles[i].position.x*scale, particles[i].position.y*scale, particles[i].position.z*scalez);

    particles[i].position += new Vector3(dx[i]*Amount.x, dy[i]*Amount.y, dz[i]*Amount.z) ;           
    i++;   
       
}
       
particleSystem.SetParticles(particles, length);

}       
}

particleSystemからGetParticlesでパーティクルの構造体を取得して 計算したものをparticleSystemにSetParticlesで書き込みます

データ更新にはLateUpdate()を使用します パーティクシステムはフレーム単位で更新されますが データが確定してから処理をしないと更新のタイミングが合わずにうまくいかないためです。

 

Unityのランタイム関数 Mathf.PerlinNoise は2Dのノイズをかえす関数なので、今回の場合は3Dに拡張したPerlin関数が必要になります。unity公式で配布されているProcedual Examplesデモの"Crumple mesh modifier"シーンがPerlinノイズを使用してランダムにサンプルをモーフさせるサンプルになっているので、そこからPerlinノイズのクラスファイルを引用しています。

f1f41dd7-d329-47da-92e8-6e8345fb72c7

■Procedural Examples  By Unity Technologies, Free

http://u3d.as/content/unity-technologies/procedural-examples/3zu

 

 

 

"Crumple mesh modifier"のコードを参考にしてPerlinノイズからパーティクルの速度を計算する本体を記述したのが以下の部分です。

    float  scalex = Time.time  * speed + 0.1365143f
    float  scaley = Time.time  * speed +     1.21688f;   
    float  scalez = Time.time  * speed +      
2.5564f;   
       
    dx[i] = noise.Noise(particles[i].position.x*scale, particles[i].position.y*scale, particles[i].position.z*scalex);
    dy[i] = noise.Noise(particles[i].position.x*scale, particles[i].position.y*scale,particles[i].position.z* scaley); 
    dz[i] = noise.Noise(particles[i].position.x*scale, particles[i].position.y*scale, particles[i].position.z*scalez);

    particles[i].position += new Vector3(dx[i]*Amount.x, dy[i]*Amount.y, dz[i]*Amount.z) ;   

時間あたりのスケールコントロールがspeed でscalexyzにそれぞれ加算される数値 x: 0.1365143f, y: 1.21688f, z:   2.5564f    はマジックナンバーのようなのでそのまま使用します。

サンプルのコード(fig2:)ではposition.z は式に定義していませんが 今回のコードでは計算に加味しています。

このPerlin関数は(0,1)の値を返す関数なのですが(Unityの2DPerlinも同じ範囲の値を返す) サンプルのコード

ではPerin関数が(−1,1)の値を返してくるという前提だと思いますので 同じような式にしたい場合はperlinノイズに perlin(***)*2-1 の補正をかけて拡張すればよいかと思います。

式によって若干パーティクルの挙動に変化が出ると思いますが、もともとが正確な計算でもないので用途に応じてそれっぽい飛び方をするように最適化すればいいような気がします。

さらに大量にパーティクルを処理させたい場合は Perlinノイズ生成部分を3Dテクスチャ化またはハッシュテーブルを使用したものに置き換えたり、GPUシェーダやコンピュートシェーダの使用 などが有効そうです。

データの調整ですがScale 値がPerlinノイズのサイズに対する数値の取得範囲だと考えてください。

ノイズテクスチャに対して値が小さければパーティクルは大きく動き 大きければ小刻みに振動します。

だいたい良い感じに調整したつもりなんですけどね。

スクリプトはデータの流れがわかりにくくなるのでベタな書き方をしていますが 変数を構造体でまとめたり わかりやすく最適化をしてみてください。

 

その他 使用法についてはPackageファイルをダウンロードして確認してみてください。

 

■参考

■Introduction To Noise Functions

http://freespace.virgin.net/hugo.elias/models/m_perlin.htm

パーリンノイズの生成方法とコードが掲載してあるサイトがありますので興味のある方は参照して理解を深めてみてください

■Noise-Based Particles, Part I at The Little Grasshopper

●Noise-Based Particles, Part I http://prideout.net/blog/?p=63

●Noise-Based Particles, Part II http://prideout.net/blog/?p=67

流体シュミレーションでは有名?なThe Little Grasshopperブログですがノイズベースパーティクルの構造が

把握できると思います。

GPU Gems - Chapter 5. Implementing Improved Perlin Noise

GPU Gems - Chapter 38. Fast Fluid Dynamics Simulation on the GPU

GPU Gems - Chapter 26. Implementing Improved Perlin Noise



akinow at 18:47|PermalinkComments(0)TrackBack(0) Clip to Evernote

2013年09月27日

うにばな(トリガーを使用した最適化)

 

テラシュールウェアさんのところで、ヒットチェックに関する記事が書かれていましたので補足的なことを解説してみようと思います。

内容はリンク先を参照してみてください。

 

[Unity]地面の接地判定を取得する3つの方法

http://terasur.blog.fc2.com/blog-entry-555.html

・CharacterControllerのisGroundを使う
・CheckSphereを使う
・SphereCastを足元に撃つ

能動的にスクリプトでコリジョン判定を確認する場合はこの例のような感じになるとおもいます。

以前も最適化の記事でPhysicsタイプの関数は全体的に重た目なのでなるべく限定して使用したいということを記述してきましたが、ゲームエンジンのスクリプトがなぜ速度をあまり必要としないかという解説が公式のアンリアルスクリプトの項目に寄せられていまして、 ゲームのスクリプトが他のアプリケーションのスクリプトと異なる点は基本的に待機関数がメイン つまり何かの行動を起こした時に実行される関数が処理の大半を占めるためそれほど高速に動作しなくてもゲームは成立するという説明がされています。

Physics関数は基本的にUpdate内部でコールしますが レイキャストなどのコリジョンチェックはフレームごとに呼び出されるためにゲームの実行速度に影響が出やすいです。 これを回避するための方法としてはWaitForSeconds()を使用してレイキャストの頻度をコントロールする さらにレイキャストの距離を短めに設定して必要ないヒットチェックをしないことなどヒットチェックを最小限に抑える方法があります。

ただしあまり レイキャストの頻度を下げてしまうと オブジェクトが高速に移動する場合にヒットチェックをすり抜ける可能性も考えられますので極端にレイキャストの頻度を減らすことは出来ません。

そこでPhysics関数の弱点を補ってPhysics関数をなるだけつかわない例もありますというのを解説します。

 

■トリガーを使用したコリジョンのヒット判定

Unity3Dでの待機関数というと名前が ”On~ ”で始まる関数全般ですが、 これを積極的に使用して重た目な関数は補助的に使用することで、最適化が期待できると考えられます。

待機型関数の場合はヒットチェックが実行中常に監視されていて 能動的に使用するPhysics関数と比べてコライダーを検出するだけのシンプルな動作で比較的動作が軽いです。

そこでトリガーを使用したヒットチェックの例を解説します。

EXAMPLE1:

TriggerSample1

図のようにキャラクターコントローラやキャラクターモーターを中心にトリガーを効率よく配置して、それぞれのトリガーにスクリプトを設定します。

スクリプト例:

static var triggered=0;

function OnTriggerEnter  (other : Collider) {
triggered=1;
}

function OnTriggerExit (other : Collider) {
triggered=0;
}

 

とても単純なスクリプトですが周囲のコリジョンコライダーにヒットした時、離れた時にフラグのOn、Offを行います。

フラグはキャラクターコントローラーに設定したスクリプトからトリガーのフラグ変数を読み取ってもよいですが、キャラクターコントローラにフラグをまとめてしまって各トリガーに状態を書き込んでもらったほうが見やすくなりそうな気もします。またはStaticタイプを指定すれば外部からも参照しやすくなります 使いやすく工夫してみてください。

static型は使用しない時 nullを放り込んでおけば実質メモリ消費がないはず

トリガーの設置位置はAIのステアリング操作ならば左右斜め前方に配置するとか 壁との距離を一定に保ちたい場合は左右にそれぞれ配置するなど うまくいく配置を試す必要があると思います。

もちろん配置した後ゲームの進行に合わせてトリガーを移動させたりスケールをかけたりゲームにおけるコリジョンの使い方と同じようにするのは自由です。格闘ゲームでは技によって判定が大きくなったり小さくなったりしますよね。

 

EXAMPLE2:

 

hittrig

そのほかに 敵との距離に応じてAIで状態遷移をする場合などは下図のようにキャラクター全体を覆うトリガーでヒットチェックを行うことでレイキャストの使用を避ける事が出来ます

 

TrigHitArray

図のように複数の敵が存在してそれぞれがプレイヤーの攻撃エリア(トリガー)に接触する場合。

攻撃をするための配列を用意して接触時OnTriggerEnterが有効になった時に配列に敵の情報をプッシュし

トリガーから敵が離脱した時に、その敵の情報(IDやTransform)をリムーブすることで効率よく索敵ができます。

その後の攻撃は一旦配列に取り込んだ敵だけを対象にすればよいため無駄にレイキャストをすることもなくスクリプトの高速化につながります。

●Physics.OverlapSphere

配列に取り込む処理を簡略化するのにPhysics.OverlapSphereを使用することも出来ます。

これは球型の判定を発生させて球内部のゲームオブジェクトを配列に格納したい場合に使用します。

例えば 球型の爆発によりエネミーにダメージを与えたい場合は配列を読み取ってそれぞれのゲームオブジェクトに対してダメージを加算します。 またタワーデフィフェンス系ゲームならば攻撃エリアにはいった敵を一気に配列に格納することで レイキャストで距離を判定しなくても配列を参照してゲームオブジェクトのTransform を参照するだけで攻撃有効な距離(例えば一番近くにいる敵)の計算が出来ます。OnTriggerEnter()に仕込むと良いと思いますが処理速度の実測をしていないので自分で配列管理するのとどちらが良いのかははっきりとはわかりません。

ダメージを与えるだけなら敵にスクリプトを仕込んでSendMessageとばしてもいいんですけど 敵が多いと重くてね。。

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour {
    void ExplosionDamage(Vector3 center, float radius) {
        Collider[] hitColliders = Physics.OverlapSphere(center, radius);
        int i = 0;
        while (i < hitColliders.Length) {
            hitColliders[i].SendMessage("AddDamage");
            i++;
        }
    }
}

EXAMPLE3:実装例

dotaSample

以前製作していた DOTA系のミニオンキャラクターのトリガーの配置ですがゲームのルール上 道で敵と出会った時にキャラクター同士すれ違いができないように 道全体をカバーするようにトリガーを配置してトリガー同士が重なる時にヒットチェックがでるようになっています。敵と自分の間に遮蔽物がある場合があるので、敵がトリガーにヒットしたときに一回だけ相手に向かってレイキャストします。 帰ってきた情報がトリガーにヒットした相手のコライダーと同じならば、敵が見える位置にいるので攻撃に移ります。

あとは状態に応じてトリガーのスケールなどを変化させることで細かい調整をしています。ここらへんはトライアンドエラーですね。 レイキャストだけの使用だとすり抜けが起きてしまうのでこのような構造になりました。

ちなみにこのゲームは諸事情あって棚上げになってます。

製作過程を見たい方は 過去ログを参照してみてください。



akinow at 20:00|PermalinkComments(0)TrackBack(0) Clip to Evernote

2013年09月03日

うにばな (シェーダのカラーコレクト・ブレンドに関する)

 

前回の記事で使用したPhotoshop のカラー合成の式をHLSL化したコードを使用していましたが

記事が長すぎてその部分の解説ができませんでした。

現在コードが掲載されていたリンク先が落ちてしまっているようなのでコードだけ転載します。

もとがライブラリ.hファイルなので 下の方の関数は上のほうを参照していますので Shader内に記述するときは

上から下への並びを入れ替えずに書き加えて下さい。

デザイナーさんからPhotoshopと同じカラー合成モードの見た目をお願いされることも多々あると思いますので

そんな場面にも使用してみるといいかと思います。

 

●ソースリンク先

http://www.pegtop.net/delphi/articles/blendmodes/
http://blog.mouaif.org/2009/01/05/photoshop-math-with-glsl-shaders/

 

 

 

■ Desaturate (彩度を下げる)

float4 Desaturate(float3 color, float Desaturation)
{
    float3 grayXfer = float3(0.3, 0.59, 0.11);
    float grayf = dot(grayXfer, color);
    float3 gray = float3(grayf, grayf, grayf);
    return float4(lerp(color, gray, Desaturation), 1.0);
}

 

■ RGBToHSL (RGB値 からHSL値(色相・彩度・明度)に変換)

float3 RGBToHSL(float3 color)
{
    float3 hsl; // init to 0 to avoid warnings ? (and reverse if + remove first part)
   
    float fmin = min(min(color.r, color.g), color.b);    //Min. value of RGB
    float fmax = max(max(color.r, color.g), color.b);    //Max. value of RGB
    float delta = fmax - fmin;             //Delta RGB value

    hsl.z = (fmax + fmin) / 2.0; // Luminance

    if (delta == 0.0)        //This is a gray, no chroma...
    {
        hsl.x = 0.0;    // Hue
        hsl.y = 0.0;    // Saturation
    }
    else                                    //Chromatic data...
    {
        if (hsl.z < 0.5)
            hsl.y = delta / (fmax + fmin); // Saturation
        else
            hsl.y = delta / (2.0 - fmax - fmin); // Saturation
       
        float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;
        float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;
        float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;

        if (color.r == fmax )
            hsl.x = deltaB - deltaG; // Hue
        else if (color.g == fmax)
            hsl.x = (1.0 / 3.0) + deltaR - deltaB; // Hue
        else if (color.b == fmax)
            hsl.x = (2.0 / 3.0) + deltaG - deltaR; // Hue

        if (hsl.x < 0.0)
            hsl.x += 1.0; // Hue
        else if (hsl.x > 1.0)
            hsl.x -= 1.0; // Hue
    }

    return hsl;
}

 

■ HueToRGB(色相からRGB値を計算)

float HueToRGB(float f1, float f2, float hue)
{
    if (hue < 0.0)
        hue += 1.0;
    else if (hue > 1.0)
        hue -= 1.0;
    float res;
    if ((6.0 * hue) < 1.0)
        res = f1 + (f2 - f1) * 6.0 * hue;
    else if ((2.0 * hue) < 1.0)
        res = f2;
    else if ((3.0 * hue) < 2.0)
        res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;
    else
        res = f1;
    return res;
}

 

■ HSLToRGB(HSL値 からRGBに変換)

float3 HSLToRGB(float3 hsl)
{
    float3 rgb;
   
    if (hsl.y == 0.0)
        rgb = float3(hsl.z, hsl.z, hsl.z); // Luminance
    else
    {
        float f2;
       
        if (hsl.z < 0.5)
            f2 = hsl.z * (1.0 + hsl.y);
        else
            f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);
           
        float f1 = 2.0 * hsl.z - f2;
       
        rgb.r = HueToRGB(f1, f2, hsl.x + (1.0/3.0));
        rgb.g = HueToRGB(f1, f2, hsl.x);
        rgb.b= HueToRGB(f1, f2, hsl.x - (1.0/3.0));
    }
   
    return rgb;
}

 

■コントラスト・彩度・明度 の調整

float3 ContrastSaturationBrightness(float3 color, float brt, float sat, float con)
{
    // Increase or decrease theese values to adjust r, g and b color channels seperately
    const float AvgLumR = 0.5;
    const float AvgLumG = 0.5;
    const float AvgLumB = 0.5;
   
    const float3 LumCoeff = float3(0.2125, 0.7154, 0.0721);
   
    float3 AvgLumin = float3(AvgLumR, AvgLumG, AvgLumB);
    float3 brtColor = color * brt;
    float intensityf = dot(brtColor, LumCoeff);
    float3 intensity = float3(intensityf, intensityf, intensityf);
    float3 satColor = lerp(intensity, brtColor, sat);
    float3 conColor = lerp(AvgLumin, satColor, con);
    return conColor;
}

 

■ カラー合成モードいろいろ

1. Photoshopのカラー合成モードを再現した式のファンクション定義

#define BlendLinearDodgef             BlendAddf
#define BlendLinearBurnf             BlendSubstractf
#define BlendAddf(base, blend)         min(base + blend, 1.0)
#define BlendSubstractf(base, blend)     max(base + blend - 1.0, 0.0)
#define BlendLightenf(base, blend)         max(blend, base)
#define BlendDarkenf(base, blend)         min(blend, base)
#define BlendLinearLightf(base, blend)     (blend < 0.5 ? BlendLinearBurnf(base, (2.0 * blend)) : BlendLinearDodgef(base, (2.0 * (blend - 0.5))))
#define BlendScreenf(base, blend)         (1.0 - ((1.0 - base) * (1.0 - blend)))
#define BlendOverlayf(base, blend)     (base < 0.5 ? (2.0 * base * blend) : (1.0 - 2.0 * (1.0 - base) * (1.0 - blend)))
#define BlendSoftLightf(base, blend)     ((blend < 0.5) ? (2.0 * base * blend + base * base * (1.0 - 2.0 * blend)) : (sqrt(base) * (2.0 * blend - 1.0) + 2.0 * base * (1.0 - blend)))
#define BlendColorDodgef(base, blend)     ((blend == 1.0) ? blend : min(base / (1.0 - blend), 1.0))
#define BlendColorBurnf(base, blend)     ((blend == 0.0) ? blend : max((1.0 - ((1.0 - base) / blend)), 0.0))
#define BlendVividLightf(base, blend)     ((blend < 0.5) ? BlendColorBurnf(base, (2.0 * blend)) : BlendColorDodgef(base, (2.0 * (blend - 0.5))))
#define BlendPinLightf(base, blend)     ((blend < 0.5) ? BlendDarkenf(base, (2.0 * blend)) : BlendLightenf(base, (2.0 *(blend - 0.5))))
#define BlendHardMixf(base, blend)     ((BlendVividLightf(base, blend) < 0.5) ? 0.0 : 1.0)
#define BlendReflectf(base, blend)         ((blend == 1.0) ? blend : min(base * base / (1.0 - blend), 1.0))

 

2. 1を使用したカラーブレンド式の定義 Shader式に組み込むにはこちらをつかいます

#define Blend(base, blend, funcf)         float3(funcf(base.r, blend.r), funcf(base.g, blend.g), funcf(base.b, blend.b))

#define BlendNormal(base, blend)         (base)
#define BlendLighten                BlendLightenf
#define BlendDarken                BlendDarkenf
#define BlendMultiply(base, blend)         (base * blend)
#define BlendAverage(base, blend)         ((base + blend) / 2.0)
#define BlendAdd(base, blend)         min(base + blend, float3(1.0, 1.0, 1.0))
#define BlendSubstract(base, blend)     max(base + blend - float3(1.0, 1.0, 1.0), float3(0.0, 0.0, 0.0))
#define BlendDifference(base, blend)     abs(base - blend)
#define BlendNegation(base, blend)     (float3(1.0, 1.0, 1.0) - abs(float3(1.0, 1.0, 1.0) - base - blend))
#define BlendExclusion(base, blend)     (base + blend - 2.0 * base * blend)
#define BlendScreen(base, blend)         Blend(base, blend, BlendScreenf)
#define BlendOverlay(base, blend)         Blend(base, blend, BlendOverlayf)
#define BlendSoftLight(base, blend)     Blend(base, blend, BlendSoftLightf)
#define BlendHardLight(base, blend)     BlendOverlay(blend, base)
#define BlendColorDodge(base, blend)     Blend(base, blend, BlendColorDodgef)
#define BlendColorBurn(base, blend)     Blend(base, blend, BlendColorBurnf)
#define BlendLinearDodge            BlendAdd
#define BlendLinearBurn            BlendSubstract

#define BlendLinearLight(base, blend)     Blend(base, blend, BlendLinearLightf)

#define BlendVividLight(base, blend)     Blend(base, blend, BlendVividLightf)
#define BlendPinLight(base, blend)         Blend(base, blend, BlendPinLightf)
#define BlendHardMix(base, blend)         Blend(base, blend, BlendHardMixf)
#define BlendReflect(base, blend)         Blend(base, blend, BlendReflectf)
#define BlendGlow(base, blend)         BlendReflect(blend, base)
#define BlendPhoenix(base, blend)         (min(base, blend) - max(base, blend) + float3(1.0, 1.0, 1.0))
#define BlendOpacity(base, blend, F, O)     (F(base, blend) * O + blend * (1.0 - O))

 

■色相のブレンド

float3 BlendHue(float3 base, float3 blend)
{
    float3 baseHSL = RGBToHSL(base);
    return HSLToRGB(float3(RGBToHSL(blend).r, baseHSL.g, baseHSL.b));
}

 

■彩度のブレンド

float3 BlendSaturation(float3 base, float3 blend)
{
    float3 baseHSL = RGBToHSL(base);
    return HSLToRGB(float3(baseHSL.r, RGBToHSL(blend).g, baseHSL.b));
}

 

■カラーのブレンド

float3 BlendColor(float3 base, float3 blend)
{
    float3 blendHSL = RGBToHSL(blend);
    return HSLToRGB(float3(blendHSL.r, blendHSL.g, RGBToHSL(base).b));
}

 

■明度のブレンド

float3 BlendLuminosity(float3 base, float3 blend)
{
    float3 baseHSL = RGBToHSL(base);
    return HSLToRGB(float3(baseHSL.r, baseHSL.g, RGBToHSL(blend).b));
}


akinow at 06:53|PermalinkComments(0)TrackBack(0) Clip to Evernote