今回は次世代機などで使用されているパーティクル描画の最適化手法である大量のパーティクルを描画するオフスクリーンパーティクルについてのまとめです。
ディファードレンダリング と リアルタイムのテクスチャレンダリング のサポートが必要になりますので
プロバージョンの使用が前提です。そして現在Unityはモバイルのディファードレンダリングをサポートしていません。
ですのでPCでの使用に限られていまいますが、プロ版をお持ちでない方も こういう高速化技術があることを覚えておいていずれ活用していただければと思います。
デイファードレンダリングがどのようなものかを知っておく必要があります 。 日本語でわかりやすかったのが
■ 西川善司の3Dゲームファンのための「KILLZONE 2」グラフィックス講座(前編)
PS3のハイクオリティグラフィックスはDeferred Shadingでキマリ!?
http://game.watch.impress.co.jp/docs/series/3dcg/20090417_125909.html
ディファードレンダリング(遅延レンダリング)とは 同時に最大8枚のバッファに 法線データ、 カラーデータ、など様々なレンダリングに必要なデータをリアルタイムレンダリングして 合成の後フレームバッファに書き込むことで高品質なクオリティの映像を得るためのテクニックです。 各バッファに加工を施すことで多用なエフェクトなどを表現出来ます。 フレームバッファに書きこむだけのフォワードレンダリングに対して 合成後に遅れて表示バッファに書きこむため 遅延レンダリングと呼ばれます。
■GPU Gems 3:Chapter 23. High-Speed, Off-Screen Particles
http://http.developer.nvidia.com/GPUGems3/gpugems3_ch23.html
Nvidiaの公式サイトのデベロッパーセクションで無料公開されているGPUGems3の内容に基づいて解説します。
パーティクルシステムは、多くの場合パフォーマンスの問題につながり、オーバードローを大量に消費するとレート
に影響します パーティクルシステムのエフェクトでは、たいていが多量のポリゴン、数百または数千が必要になります。
パーティクルエフェクトは多くの場合、あいまいでソフトな形状です。 例えば煙や霧のイメージは鋭いエッジなしで、低周波数のみを持っています。これらは視覚的な品質を損なうことなく 小さなサンプルで表すことができます(小さくレンダリングしても絵的にはあまり影響がない)。つまり低解像度のフレームバッファは十分有効ということが言えます。
■オフスクリーンレンダリング
パーティクルのサイズはメインフレームバッファのサイズの割合であるオフスクリーンレンダリングターゲットにレンダリングされます。ですが必ずしも同じサイズのバッファを必要とせず DX10から搭載されたダウンサンプリングの機能によって必要な品質のトレードオフに応じアプリケーションによってバッファの比率を選択することができます. 例えば、1/1、1/2、1/4、1/8 倍のスケールを選択できます(スケールは2の累乗に限定されません)
■オフスクリーン深度テスト
正確にシーン内の他の項目に対してパーティクルを閉塞するには深さのテストとそのサイズはオフスクリーンレンダリングターゲットと一致する深度バッファを必要とします
オフスクリーンレンダリングターゲットと一致する深度テストのための深度バッファ はメインのZバッファをダウンサンプルすることで取得します
- Deapth書き込みで通常通りにシーン内のすべてのソリッドオブジェクトをレンダリングします。
- 1の結果の深度バッファをダウンサンプリング。
- 小さなデプスバッファに対してテスト、オフスクリーンレンダリングターゲットにパーティクルをレンダリングします。
- パーティクルの描き込まれたテクスチャをアップサンプリング、メインフレームバッファに戻ってターゲットをレンダリングします。
【図1】

【図2】

float4 particlePS(VS_OUT vIn): COLOR
float myDepth = vIn.depth;
float sceneDepth = tex2D(depthSampler, vIn.screenUV).x;
float zFade = saturate(scale * (myDepth - sceneDepth));
/ /Compute (r,g,b,a), etc.
return float4(r,g,b, a * zFade);
【図1】ゲームのスクリーン解像度に対して パーティクルのレンダリング解像度を1/4でダウンサンプリングした場合
(a) ゲーム画面解像度に対しZバッファの解像度が1:1の場合 パーティクルとオブジェクトのZ値は(0、1)で比較するが解像度が同等なので輪郭に影響は出ない
(b) ダウンサンプルしたZバッファでオブジェクトとパーティクルのZを比較する場合(0 or 1 =表示するしない)で判定すると輪郭部分に図のようなジャギーが出てしまう
(c) 画面解像度 4×4ピクセル=16ピクセル中にオブジェクトが専有するピクセル数 / 16 として平均値を求め平均値 からフェード値を得る コード中では zFade = saturate(scale * (myDepth - sceneDepth)); の部分
レンダリング時にカラー成分のアルファ値に a * zFade として境界部分のピクセルをぼかすことでエッジが目立たなくなります。
■ “High-Speed, Off-Screen Particles” in Unity
http://technology.blurst.com/off-screen-particle-optimization/
オフスクリーンパーティクルのUnityでの実例を検索してみたところ 最も早期に実現しているブログが有りましたが
このブログのコードはコード全体の一部分でしかなく再現することができませんでした

var blendMaterial:Material = PostProcessingHelper.GetMaterial(Shader.Find(“Hidden/Off-Screen Particles Composite”));
var texelOffset:Vector2 = Vector2.Scale(source.GetTexelOffset(), Vector2(source.width, source.height));
Graphics.BlitMultiTap(particlesRT, source, blendMaterial, texelOffset);
上記コードが問題の部分 ブログのコメント欄でも書かれていますが
PostProcessingHelper.GetPPCamera();
PostProcessingHelper.GetMaterial();
という見慣れないメソッドがありますが これがおそらく本人用にカスタマイズしたライブラリのメソッドではないかなということですね。その部分は以下のサイトでも触れられています。
Unity Community: http://forum.unity3d.com/threads/41177-High-speed-Off-screen-Particles-in-Unity
Unity3D по-русски:http://www.unity3d.ru/distribution/viewtopic.php?f=18&t=5665
ほかにもチャレンジしたプロジェクトはないのかと検索をしたところUnityAnserのほうに寄せられていた質問の中にサンプルのプロジェクトをアップされてる方がおられました。
■UnityAnser:Question about the off screen particle rendering
Hi,
I am trying to show the smoke particles in low resolution texture and then blend back with other parts in the scene. I read the article http://technology.blurst.com/off-screen-particle-optimization/ and make a simple test project inhttps://docs.google.com/open?id=0B1nYEeuWnX0DVHRaaS1uMG1EXzA
But unfortunately, the alpha transparent of particle textures seem not working correctly, they are just showing black. Anyone can help me how to correct this?
Thank you.
Q: オフスクリーンパーティクルを試しているんだけど パーティクルに黒い縁がついてしまうので誰か直してくれない?(意訳
ということらしいです。 ※リンク先からUnityプロジェクトのアーカイブがダウンロードできます
■オフスクリーンパーティクルの手順
1.パーティクル専用のカメラを生成して パーティクルシステムをレイヤー分けして専用カメラのみに描画されるようにします。
2. リプレースメントシェーダー(Off-Screen Particles Replace.shader)によってパーティクルをテクスチャにレンダリングしzバッファともにダウンサンプリングします。 ダウンサンプリングされたZバッファはデプスバッファとしてメインフレームバッファとの合成時に背景と深度比較するために活用します。
これらのコードは OnPostRender() 内に記述します。OnPostRender()はフレームバッファへのレンダリングが終わったあとに呼ばれるメソッドです。
3・コンポジットシェーダー (Off-Screen Particles Composite.shader)で背景とパーティクルのバッファを合成します。
TestMainCamera.cs
using UnityEngine;
using System.Collections;
public class TestMainCamera : MonoBehaviour
{
public int factor = 2;
public Material blendMaterial;
public Shader HiddenReplaceShader;
public Shader CompositeShader;
public GameObject shaderCamera;
public Camera cam;
public RenderTexture particlesRT;
public RenderTexture source;
void Awake()
{
camera.depthTextureMode = DepthTextureMode.Depth;
particlesRT = RenderTexture.GetTemporary(Screen.width / factor, Screen.height / factor, 0);
}
void OnPostRender()
{
if (!enabled || !gameObject.active || !HiddenReplaceShader)
return;
if (!shaderCamera)
{
shaderCamera = new GameObject("ShaderCamera",typeof(Camera));
shaderCamera.camera.enabled = false;
//shaderCamera.hideFlags = HideFlags.HideAndDontSave;
}
cam = shaderCamera.GetComponent();
cam.CopyFrom (camera);
cam.cullingMask = 1 << LayerMask.NameToLayer("Test");
cam.backgroundColor = Color.black;
cam.clearFlags = CameraClearFlags.SolidColor;
cam.depthTextureMode = DepthTextureMode.None;
cam.targetTexture = particlesRT;
cam.RenderWithShader (HiddenReplaceShader, "RenderType");
Shader.SetGlobalVector("_CameraDepthTexture_Size", new Vector4(camera.pixelWidth, camera.pixelHeight, 0f,0f));
blendMaterial.shader = CompositeShader;
Vector2 texelOffset = Vector2.Scale(new Vector2(0f,0f), new Vector2(Screen.width, Screen.height));
Graphics.BlitMultiTap(particlesRT, null, blendMaterial, texelOffset);
}
}
Off-Screen Particles Replace.shader
Shader "Hidden/Off-Screen Particles Replace" {
Category {
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
AlphaTest Greater .01
Cull Off Lighting Off ZWrite Off Fog { Color (0,0,0,0) }
CGINCLUDE
// Upgrade NOTE: excluded shader from OpenGL ES 2.0 because it uses non-square matrices
#pragma exclude_renderers gles
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata {
float4 vertex:POSITION;
float4 texcoord:TEXCOORD0;
float4 color:COLOR;
};
struct v2f {
float4 pos:POSITION;
float3 uvz:TEXCOORD0;
float3 depth:TEXCOORD1;
float4 color:COLOR;
};
uniform sampler2D _MainTex : register(s0);
uniform sampler2D _CameraDepthTexture;
uniform float4 _CameraDepthTexture_TexelSize, _CameraDepthTexture_Size;
uniform float4 _TintColor;
v2f vert(appdata v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.color = v.color;
o.uvz.xy = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
COMPUTE_EYEDEPTH(o.uvz.z);
float3x4 mat = float3x4 (
0.5, 0, 0, 0.5,
0, 0.5, 0, 0.5,
0, 0, 0, 1
);
o.depth = mul(mat, o.pos);
#ifdef SHADER_API_OPENGL
o.depth.xy *= _CameraDepthTexture_Size.xy;
#endif
return o;
}
half4 frag(v2f i):COLOR {
half4 texCol = tex2D(_MainTex, i.uvz.xy);
half4 outCol = i.color * _TintColor * texCol * 2;
#ifdef SHADER_API_D3D9
// Directx is off by half a texel
// so we need to do the projection seperately
i.depth.xy = i.depth.xy/i.depth.z;
// then offset the uvs by 0.5*TexelSize
i.depth.xy += 0.5 * _CameraDepthTexture_TexelSize.xy;
// we also need to flip the y if in directx
i.depth.y = 1 - i.depth.y + _CameraDepthTexture_TexelSize.y;
float z = tex2D(_CameraDepthTexture, i.depth.xy).r;
#else
float z = tex2Dproj(_CameraDepthTexture, i.depth).r;
#endif
float sceneDepth = DECODE_EYEDEPTH(z);
float myDepth = i.uvz.z;
if(sceneDepth < myDepth)
discard;
return outCol;
}
ENDCG
SubShader {
Pass {
Blend SrcAlpha OneMinusSrcAlpha
ColorMask RGB
CGPROGRAM
ENDCG
}
Pass {
Blend Zero OneMinusSrcAlpha
ColorMask A
CGPROGRAM
ENDCG
}
}
}
}
一応修正を入れてみました バグといえるほどのものではないのですがシェーダーでアルファチャンネルブレンドを
指定していますが テクスチャ自体にアルファが入っていないのでコンポジットをした時に透明バッファに値が渡らないわけです。
テクスチャを変えなくてもブレンド指定で加算かソフト加算を指定していれば結果は正確になるのですが
コードに素直に従って修正します。

左側テクスチャのアルファ抜きをして右のテクスチャに変更をしました 問題点はそこだけです

Off-Screen Particles Composite.shader
Shader "Hidden/Off-Screen Particles Composite"
{
Properties
{
_MainTex ("Base (RGB)", RECT) = "white" {}
}
SubShader
{
Pass
{
ZTest Always Cull Off ZWrite Off Fog { Mode Off }
Blend One SrcAlpha
SetTexture[_MainTex] {combine texture}
}
}
Fallback Off
最終的な背景との合成は ”Off-Screen Particles Composite.shader” で行われます
(テクスチャーのコンバインだけの処理ですので説明は省きます。)
背景との合成方法はBlend に続く式で制御します パーティクルのアトリビュートではありません※赤文字部分

詳しは http://docs.unity3d.com/Documentation/Components/SL-Blend.html を参照してください
■パフォーマンスの結果について

実際にゲームに組み込んでみたところ 描画速度の向上にはかなり効果が高いようです
ゲームのスクリーン解像度が高くなるほど効果は顕著に現れます。
修正後のプロジェクトファイルをアップロードしました。 プロジェクト内でエフェクト用カメラにパーティクルのみを描画させるためのレイヤーを設定しているのですが パッケージではレイヤーの再設定が必要になるためプロジェクトファイルを圧縮してあります。
http://dl.dropbox.com/u/18978109/TestMixingResolution_re.zip
※ テスト用のスクリプトは多少アバウトに制作されていますので、使い勝手を上げるためにはシェーダーを自動検索させたり カメラを自動登録させたりといった処理は入れたほうがいいと思われます。
追記:
コードを見やすくするスクリプトをブログに組み込んではみたものの ものすごくHTMLの行数を稼いでしまうため
またいつものコードベタ張りになってしまいました。 コードはプロジェクトファイルのものをそのまま掲載していますので、そちらを参考にしてください。
あと不明な点や訪ねたいことがありましたら コメントしていただけると 調べて回答しますのでお気軽にどうぞ。
※ 最近グーグル検索に暗号化が施されるようになって検索ワードが読めないので 記事を作成する方針がたてにくくなってしまっています。
あと 記事が検索にかかりやすくと思ってつけた ”うにばな” ですが ”うに ランク” などで検索があったりして思わぬ落とし穴でした 水産加工関係の方申し訳ありません (´・ω・`)
ちなみに管理人はウニ大好きです大好きです


