以前にGrabPass命令の解説をしましたが、それに関する補足説明とそれに関連してシェーダのUnityでのスクリーンスペース系の演算について補助的に解説をします。
Grab命令自体はイメージエフェクト処理の実行速度が解像度に比例して高コストになるため これを回避する方法として出来る限りフォワードベースに計算を移し替えてしまう方法で処理負荷の軽減を図るという意味では非常に有効な機能です 特に非力なモバイル系などで効果が期待できるのですが
ところが公式に実装された機能であるGrabPass命令が、それほど実行速度が早くないという問題があります。レンダーテクスチャをシェーダにセットしたほうがGrabPass命令に比べて処理速度が早いのですね。
そこでレンダーテクスチャとコマンドバッファーを組み合わせて最適化する方法があります。
"RenderingCommandBuffers50b22” のデモ”CommandBufferBlurRefraction.cs”と"GlassWithoutGrab.shader" のサンプルがそれです 。参考にコードを記載します、
”CommandBufferBlurRefraction.cs"
using UnityEngine; // See _ReadMe.txt for an overview private Camera m_Cam; // We'll want to add a command buffer on any camera that renders us, // Remove command buffers from all cameras we added into public void OnEnable() public void OnDisable() // Whenever any camera will render us, add a command buffer to do the work on it CommandBuffer buf = null; if (!m_Material) buf = new CommandBuffer(); // copy screen into temporary RT buf.SetGlobalTexture("_GrabBlurTexture", blurredID); cam.AddCommandBuffer (CameraEvent.AfterSkybox, buf);
// copy screen into temporary RT
|
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;
”fragment shader” |
すでに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
- http://forum.unity3d.com/threads/optimize-shader.328540/ 詳しい解説はリンク先を参照してください
■ _MainTexのフェッチ(読み込み)はなるべくシェーダの先頭に記述すること。_MainTexは大部分のシェーダで記述されていますが,シェーダの計算時に頻繁にアクセスされると実行速度の低下を引き起こすため シェーダ内で多くの計算を行う前にデータフェッチを完了しておきます。
■ _GrabTextureのフェッチはシェーダのなるべく最後の方に記述します。シェーダがGrabPass命令を受けてからtextureをレンダリングしてGPUにセットする動作を完了するまでなるべく時間をかせいでおくこと。GrabTextureのフェッチ前にGrabTextureのレンダリングが修了していない場合シェーダは待機状態になってしまい処理速度が低下します。
最適化前:
half4 frag( v2f i ) : COLOR float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy; |
最適化後:
half4 frag( v2f i ) : COLOR packedNormals = tex2D( _BumpMap, i.uvbump ); half4 tint = tex2D( _MainTex, i.uvmain ); //Grabテクスチャからサンプリングを行う前に, 多くの計算式を記述してしまうこと half2 bump = UnpackNormal(packedNormals).rg; |
例の最適化前の例ではMainTextureとGrabTextureの位置が逆に記述されています。 このようにフォワードシェーダではレンダリングテクスチャを使用する場合に少し調整をする必要があります。
■補足解説
テクスチャサンプラーの参照は通常のシェーダ関数の計算に比べて100倍程度重たい処理となるので(クロック値ですね)なるべくテクスチャ参照回数を減らすことが演算速度の向上につながります。ブラーシェーダを例に上げると5×5のカーネルのフィルターをそのまま1パスで実装すると1ピクセルあたり25回のテクスチャ参照が行われることになりますが、 縦方向と横方向(VとH = V,U)の2パスに分離することで縦方向に5回+横方向に5回の10回のテクスチャ参照に抑えることができるためシェーダの処理速度は向上します。 レンダリングパスは増加しますがテクスチャ参照回数を減らすほうがメリットが有る場合があるわけです.
GPU演算はCPUに比較すると4,50倍程度演算能力は高いので軽量なシェーダを回す分には差が感じられないかもしれませんが スクリーン全体のエフェクトなどのように描画ピクセル数が増えると最適化の恩恵が出てくるわけです。画面表現がシンプルなゲームではそれほど最適化に気をつかう必要はないと思います。
以上のような手法で画面全体をレンダリングするイメージエフェクトはなるべくフォワードベースのシェーダに変換してしまえば、非力なGPUハードウェアでも ある程度負荷の高いエフェクト処理をゲーム制作に活用することができます。もちろんディファードレンダリングをメインに使用する場合でもディファードレンダリングの苦手な処理を別カメラでフォワードレンダリングでレイヤー合成すれば 描画負荷は軽減できます。
■ここからスクリーンスペース系のシェーダに関する解説を追加しておきます
すでにご存知の情報もあると思いますが、これから学習する人もいるので
おおまかにスクリーンスペース系のシェーダの流れはものにもよりますが以下のようになります
- スクリプト内:カメラ行列の再計算→シェーダにセット
- depth、normalテクスチャからワールド座標など必要なデータをを再計算
- レンダリング後のテクスチャをブラーパスでぼかす
- 背景キャプチャしたテクスチャをレイヤ合成する
以前の記事でもシェーダの行列について解説はしているのですがもう一度おさらいをします。
- オブジェクト座標 = 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; if (d3d) { float4x4 MVP = P*V*M; // OpenGLとD3Dでは行列を掛け合わせる方向が逆になります Shader.SetGlobalMatrix("_matMVPI", MVP); float4x4 ModelViewProjI = _matMVPI; } |
【Script側】(現在はこのように記述してください 必要なものだけでいいです)
void Update()
Shader.SetGlobalMatrix("_ProjectionMatrix", projectionMatrix); } |
- _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)) float4x4 cofactors = float4x4( -minor(_12_13_14, _32_33_34, _42_43_44), minor(_12_13_14, _22_23_24, _42_43_44), -minor(_12_13_14, _22_23_24, _32_33_34), |
- http://www.cg.info.hiroshima-cu.ac.jp/~miyazaki/knowledge/tech23.html
- http://answers.unity3d.com/questions/218333/shader-inversefloat4x4-function.html
■GL系とDirectX系の差異の解消
GL系とDirectX系でのシェーダ内で記述する際のポイントをあげてみます
■スクリーンスペース上でのベクトルはGLとDirectXはUVのV値が上下反転されているので DirectXの場合 uvはy=1-uv.y ベクトルには_ProjectionParams.を掛けることで正確な値となります。
- Direct3Dでは、座標は上部がゼロで下向きに増大します。
- OpenGLとOpenGL ESでは、座標の下部がゼロで上向きに増大します。
■GLとDirectXのテクスチャ座標が上下反転の解消 #if defined(SHADER_API_D3D9) || defined(SHADER_API_D3D11) ■ベクトルの上下反転の解消 #if defined(SHADER_API_D3D9) || defined(SHADER_API_D3D11)vector.y *= _ProjectionParams.x; #endif
_ProjectionParams はそれぞれ
|
■HLSL とGLでは行列の扱いが異なります
AAAA
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/ でシェーダ記述の練習をするために書かれた記載です。
- 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); |
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) float3 GetWorldPos (float3 screenPos) float3 GetViewPos (float3 screenPos)
|
GetScreenPos (float2 uv, float depth) :スクリーンUVとdepthを与えてスクリーン座標が出れば
それをカメラの逆行列にかければワールド座標とビュー座標のポジションが確定します。 後必要なのはビューベクトルで
normalize(worldPos - _WorldSpaceCameraPos)でビュー正規化ベクトルが取得できますので、これにdepthをかけるとワールドのポシションが参照できます。
worldPos.xyz / worldPos.w; のwは何かというとプロジェクション行列の場合は
- その55 そもそも「w」って何なのか?http://marupeke296.com/DXG_No55_WhatIsW.html
カメラ奥行きに対して頂点座標ごとのフラスタム(画角ですね)のサイズ比率が帰ってくるので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) sampler2D _MainTex; sampler2D_float _CameraDepthTexture;
i.ray = i.ray * (_ProjectionParams.z / i.ray.z); clip (float3(0.5,0.5,0.5) - abs(opos.xyz)); ※Unity4.6から sampler2D_float _CameraDepthTexture; のように サンプラーに_floatをオプションで記述することで テクスチャサンプラーで強制的に型を指定することができるようになりました 高い精度が必要な場合は指定してください。
|
■Depth値から法線の再計算
法線情報はdepthnormalテクスチャから取得できるので必要では無いのですけど 追加情報ということで書いておきます。
GLコードのままでもうしわけないですが。 プロシージャルなイメージなどの場合に法線の再計算が必要な場合が出てくるので そうした用途です・
a: //w = linear depth { b: vec4 pos = vec4((gl_TexCoord[0].st - depth_size.xy * 0.5) / (depth_size* 0.5), w, 1.0) * w * gl_ProjectionMatrixInverse; c:
|
■TANGENT_SPACE_ROTATIONを使用したベクトル計算
grabテクスチャを使用したフォワードベースのシェーダでスクリーンスペース計算を行うときはTANGENT_SPACE_ROTATIONの使用が適しています。
AssetStoreの『HARDSURFACE SHADER』『Candela SSRR 』などがこの方法でシェーダー実装されていると公式フォーラムでアドバイスがされていたのですが 現在では前者はPROバージョンのサポートが終了してしまい現在はコード難読化、 後者はデフォルトで難読化されていて確認が困難なため、実装について簡単に解説をしておきます。
struct v2f {
TANGENT_SPACE_ROTATION;
|
これにはUnify Community WikiのMatCap シェーダで使用されている方法を応用します。いちおう解説を
TANGENT_SPACE_ROTATION マクロを指定して2D座標系(スクリーン上のタンジェントスペース)の回転ベクトルに変換するための行列を作成しています。タンジェントスペースというのは法線に対して直行するテクスチャ上のUV座標系のことです 要は通常は法線からテクスチャのUVを求める関数をスクリーン上のUV計算に応用しているわけです。
■スクリーンスペース系(ビュー座標)でベクトルの扱い
例としてビュー座標系のベクトル計算のサンプル一覧をマニュアルから抜粋します。
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); |
half2 absvn = abs(vn); //ビューノーマルを絶対値に変換 half maxvn = max(absvn.x,absvn.y); //ベクトル長さの大きな方を選択 maxvn = 1 / maxvn; //ベクトルの長さで割り算 vn = vn * maxvn; |
アスペクト比を吸収しますUV長さ(1,1)はゲーム画面で通常横長楕円になるのでベクトルの長い成分の絶対値で割ります
ベクトルの最大長さは1で帰りますがそのまま計算に使用するとスクリーンから溢れてしまうためスクリーン座標の縦横のサイズの大きな方で割ることでベクトルの長さをピクセルサイズ以下の大きさに調整します。イメージなので実際はもっと小さい円です。
ベクトルの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乗で減衰する |
出力カラーは最終的にフォールオフ値を掛けて調整しますが、スクリーンスペース系での計算は正確なものではないため以下のような条件を回避するために減衰を設定して見栄えを良くする必要があります。
- 画面の端 UV値が0~1範囲の外からカラーを取得するとスクリーンをレンダリングしたテクスチャの端はロールアップされて引き伸ばされているため スクリーン画面端に向かってカラーを減衰する。
- カメラビューに対して奥行き方向に歪みを軽減するためにDepth値に応じて取得するカラーを減衰する。
- 法線がカメラ方向に向かっている場合。モデル表面にモデル自身が写り込んでしまうためカメラビューと法線の内積を計算してカラーを減衰させる。 手前方向など目立つところは通常の反射キューブマップ で補完
- カラー取得の座標距離が離れるほど画像が伸びてしまうので距離に応じてカラーを減衰させる。
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 v2f o; // color.a が空いているのでspecuar値などを追加してもよい |
■ブラーパス及びテクスチャのレイヤー合成に関しては通常のシェーダと特にかわるところはありませんので、それぞれの該当するシェーダを参考にしてください 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 int numSamples = 7; float4 result = 0.0; 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.
|
■ リファレンス
- Real-Time Local Reflections - GameDev.net
- DirectXの話 第140回 - もんしょの巣穴
- Screen Space Local Reflection - Unity Community
- Screen space raytracing and depth buffer sample filtering. - Unity Community
- http://learnopengl.com/#!Getting-started/Coordinate-Systems
- Cg Programming-Unity-Shading in World Space - Wikibooks, open books for an open world
- 0013-04-07 - Nao_uの日記 - Game Programmerグループ 空間とプラットフォームの狭間で ? Unityの座標変換にまつわるお話 ?
- https://alastaira.wordpress.com/2015/08/07/unity-shadertoys-a-k-a-converting-glsl-shaders-to-cghlsl/
- https://docs.unity3d.com/Manual/SL-PlatformDifferences.html
- https://msdn.microsoft.com/en-GB/library/windows/apps/dn166865.aspx
- Depth Textureの使用 / Using Depth Textures - Unity Manual
- http://forum.unity3d.com/threads/solved-dynamic-blurred-background-on-ui.345083/
- https://www.assetstore.unity3d.com/jp/#!/content/6296
- https://software.intel.com/en-us/blogs/2014/07/15/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms
今回もちょっと長めになってしまいました もと記事を書いたのが2年近く前だったので無駄な文章が散見されますが 頑張って読んで下さってあり難うございます 時間が許せば参考にコードもアップしていけるといいかも。 正直長すぎて途中でよくわからなくなったのでコレで理解出来るのか疑問ですが他のサイトも参考にすればだいじょうぶ… でしょう
シェーダ周りのめんどうな規則はUnityが汎用エンジンを目指したことで 通常ならライブラリ開発サイドがコントロールする部分をゲーム開発側に対応を任せることになり敷居が上がってしまうというコンセプトの問題点が表面に出てきた感じでしょうか。
スクリプトを前面に押し出すと主なターゲットにするはずだったゲーム開発初心者は腰が引けてしまうのでUnrealEngineのようなうまい感じに目隠しをする方向が理想的な気はします。そこら辺は今後の改良に期待ですか 。。