ScriptableRenderLoop概要
今までは情報がある程度まとまってから書いてたけど 試しに気にせずに毎日でも書くことにしたよ すぐ飽きるけど
ScriptableRenderLoopとは
UNITY2017から入る機能の1つで、現在はベータ版でお試しが可能 今までブラックボックスだった UNITYのレンダリングパイプラインをスクリプトでカスタマイズできる機能 具体的に何ができるかといわれると 難しいけど、G-Bufferとかライト計算とかいろいろと自由にできる
今までのRenderingPipeline
今までは レンダリングモードが Forwardの時は ForwardBase、Deferredは Deferred が実行されていた Unity - マニュアル: Unity のレンダリングパイプライン マルチパスは シェーダーにて 複数パスを書いて 暗黙的に実行させる ライトモードにより、実行するシェーダーパスは固定であり レンダリングパイプラインは変更する事が不可能
グラフィックスコマンドバッファ
Unity - マニュアル: グラフィックスコマンドバッファ
拡張され、指定されたタイミングで任意のGPU命令を実行させることが可能になった 上記の図の緑色の地点に コールバック関数を差し込むことが出来る 例えば ポストプロセスを行う時によく使われる
ただし、レンダリングパイプライン自体は変更する事が出来ない
ScriptableRenderLoop
まず シェーダーのパスを任意に実行可能。マテリアル事、ライト毎にパスを変える事も可能 また レンダリングパイプライン自体も自由に書くことが出来る そのため G-Bufferも自由に設計可能だし、ライトの計算なども自分で行う必要がある カリングやオブジェクトのソート等の一部の機能は 変更する事は出来ない
UNITYことはじめ~ShaderLab あたり
UNITY歴
実はあまり触ってない。プラグイン書く仕事はしたけど C++をバインディングしたり、そんなのだし 一応簡単に画面を触るけど 正直表面すら理解してないので はじめてUNITYをちゃんと勉強する
幸いなことに、仕事でUNITYを使う機会があり、今回は深い部分を調査する R&D案件だ
ShaderLab
なぜ最初にShaderか? 一般的に一番難しそうではないか? だからこそ、ここを理解する間には UNITYの全部が理解できるはずである なんせ 心臓部であるので
ShaderLabって?
基本的に HLSLである。一部セマンティクスや型が違ったりするけど そのHLSLをラップして、 DirectX9の FXファイルのような感じで、パスを制御したり色々できる
意外と機能がおおいので ゆっくり勉強する
基礎構文とかはヘルプみて
シェーダーの種類
サーフェースシェーダ、頂点シェーダとフラグメントシェーダ、固定関数シェーダ の3個がある 固定関数シェーダーは ご存知DirectX9以前にあったやつで、今の時代は使う事はないだろう
頂点シェーダとフラグメントシェーダは 一般的なシェーダーで ご存知のもので、 想像通りのものである ドメインシェーダ、ハルシェーダ、ジオメトリシェーダ、コンピュートシェーダも記述できる
問題のサーフェースシェーダだが、結論からいうと 頂点シェーダとフラグメントシェーダを簡単に書くものである pragmaで Lambertとか指定すると、自動的に 頂点シェーダとフラグメントシェーダを生成してくれているようだ もちろん、細かい制御は出来ないので その時は自力で書こう
Aboutシェーダ
細かいのはヘルプ見てもらうとして 頂点&フラグメントシェーダのサンプルを
Shader "Unlit/SimpleUnlitTexturedShader" { Properties { // we have removed support for texture tiling/offset, // so make them not be displayed in material inspector [NoScaleOffset] _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } Pass { Name "FORWARD" Tags { "LightMode"="ForwardBase" } Cull Back CGPROGRAM // use "vert" function as the vertex shader #pragma vertex vert // use "frag" function as the pixel (fragment) shader #pragma fragment frag // vertex shader inputs struct appdata { float4 vertex : POSITION; // vertex position float2 uv : TEXCOORD0; // texture coordinate }; // vertex shader outputs ("vertex to fragment") struct v2f { float2 uv : TEXCOORD0; // texture coordinate float4 vertex : SV_POSITION; // clip space position }; // vertex shader v2f vert (appdata v) { v2f o; // transform position to clip space // (multiply with model*view*projection matrix) o.vertex = mul(UNITY_MATRIX_MVP, v.vertex); // just pass the texture coordinate o.uv = v.uv; return o; } // texture we will sample sampler2D _MainTex; // pixel shader; returns low precision ("fixed4" type) // color ("SV_Target" semantic) fixed4 frag (v2f i) : SV_Target { // sample texture and return it fixed4 col = tex2D(_MainTex, i.uv); return col; } ENDCG } } }
Shader “Unlit/SimpleUnlitTexturedShader” シェーダー名を設定。UNITY Editorから参照するときの名前
Properties マテリアルで設定するパラメータ テクスチャとか色々
SubShader シェーダーの始まり
SubShader/Tags 不透明オブジェクト、半透明オブジェクト等 シェーダーの設定をKeyValue方式で設定
SubShader/Pass レンダリングパス カリングやZTestなども設定できる
SubShader/Pass/Name 他のシェーダーから呼ぶ時に指定する名前
SubShader/Pass/Tags シェーダーの設定をKeyValue方式で。 例えば “LightMode”=“ForwardBase" であれば、Forwardレンダリングの時に使われるパスである Deferred等複数のパスを作れば、その時のレンダリングモードに応じたパスを実行する
CGPROGRAM~ENDCG シェーダーの具体的な中身。だいたいHLSL
pragma vertex vert pragma fragment frag 頂点シェーダ、フラグメントシェーダを使う&関数名を設定。 サーフェースシェーダだとここが surfaceになる。 その他 pragmaは色々あるのでヘルプ参照
などなど
とりあえず ヘルプに詳しく書いている
【UNITY】PostProcess
イメージエフェクト
ってUNITYでは 言うらしいよ ポストプロセスの事
概要
ポストプロセスに関しては ご存知と思うが UNITYは基本的に オブジェクトにスクリプトをくっつけていくタイプだ
UNITYではポストプロセスはカメラにつけるっぽい
MonoBehaviour.OnRenderImage(RenderTexture,RenderTexture) を実装したスクリプトを、カメラと同じ階層のGameObjectに入れれば ポストプロセスとして成立する
スクリプト作成
using UnityEngine; [RequireComponent(typeof(Camera))] //public abstract class Compose : MonoBehaviour public class Compose : MonoBehaviour { private Material _material; public string ShaderName; protected Material Material { get { return _material; } } protected virtual void Awake() { Shader shader = Shader.Find(ShaderName); _material = new Material(shader); } protected virtual void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, destination, _material); } }
上記でかいたように OnRenderImageを実装する
sourceがエフェクト前の画像であり destinationにポストエフェクト適応させた出力を行う
ShaderNameプロパティで指定したシェーダーを読み込み それを実行している
Shaderは割愛。好きに書けばいい
スクリプト配置
GameObjectを作りそこにスクリプトをアタッチ そして 同じレイヤーにカメラを置けば完成
【UNITY β機能】ScriptableRendererLoop 使ってみる
ScriptableRendererLoopとは
Unity 2017から搭載される予定の機能で現在はβバージョンである 今までブラックボックスだったレンダリングのループをある程度カスタマイズできる
カリング、Depthクリア、不透明描画、半透明描画・・・ 等の処理を変更できる サンプルによると HDRやGIや色々と高度な処理ができるようである
まずはお試し
UNITYβ版ダウンロード
この機能を試すにはβ版が必要だ
Unity - Beta から ダウンロードしよう。
過去バージョンを取得する事も可能だが私は Unity 2017.1.0b8 を使った。
ScriptableRendererLoopサンプル ダウンロード
https://github.com/Unity-Technologies/ScriptableRenderLoop/tree/unity-2017.1b5 ここにサンプルがある。 TagでUNITYのバージョンを選びダウンロードするが、エディタを2017.1.0b5にしても同じビルドエラーだった
ビルドしたらエラーが出るので 今回は無慈悲にエラーをコメントアウトした おそらく Experimental.PostProcess のファイルが欠乏しているだけとおもわれるが
HDRRenderPipeline.csの using UnityEngine.Experimental.PostProcessing; と、 m_PostProcessContext にまつわる部分を全部消した
とりあえずビルドできるようになった
ちらコード解析
BasicRenderPipeline.cs をみてみる
public static void Render(ScriptableRenderContext context, IEnumerable<Camera> cameras) { foreach (var camera in cameras) { // Culling CullingParameters cullingParams; if (!CullResults.GetCullingParameters(camera, out cullingParams)) continue; CullResults cull = CullResults.Cull(ref cullingParams, context); // Setup camera for rendering (sets render target, view/projection matrices and other // per-camera built-in shader variables). context.SetupCameraProperties(camera); // clear depth buffer var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, false, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); // Setup global lighting shader variables SetupLightShaderVariables(cull.visibleLights, context); // Draw opaque objects using BasicPass shader pass var settings = new DrawRendererSettings(cull, camera, new ShaderPassName("BasicPass")); settings.sorting.flags = SortFlags.CommonOpaque; settings.inputFilter.SetQueuesOpaque(); context.DrawRenderers(ref settings); // Draw skybox context.DrawSkybox(camera); // Draw transparent objects using BasicPass shader pass settings.sorting.flags = SortFlags.CommonTransparent; settings.inputFilter.SetQueuesTransparent(); context.DrawRenderers(ref settings); context.Submit(); } }
なるほど、ここがRenderLoopで、ここのソースを変更する事で独自のRenderLoopを作る事が出来るようだ 中身は CommandBufferを使って 描画命令をQueueに投げている
新規プロジェクトに組み込む方法
新規プロジェクトに ScriptableRendererLoopを組み込むのは少し面倒だったのでメモする
環境
Unity 2017.1.0b8 で試した。
スクリプトの追加
サンプルのBasicRenderPipeline.csをそのまま拝借する
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Experimental.Rendering; // Very basic scriptable rendering loop example: // - Use with BasicRenderPipelineShader.shader (the loop expects "BasicPass" pass type to exist) // - Supports up to 8 enabled lights in the scene (directional, point or spot) // - Does the same physically based BRDF as the Standard shader // - No shadows // - This loop also does not setup lightmaps, light probes, reflection probes or light cookies [ExecuteInEditMode] public class BasicRenderPipeline : RenderPipelineAsset { #if UNITY_EDITOR [UnityEditor.MenuItem("RenderPipeline/Create BasicRenderPipeline")] static void CreateBasicRenderPipeline() { var instance = ScriptableObject.CreateInstance<BasicRenderPipeline>(); UnityEditor.AssetDatabase.CreateAsset(instance, "Assets/BasicRenderPipelineTutorial/BasicRenderPipeline.asset"); } #endif protected override IRenderPipeline InternalCreatePipeline() { return new BasicRenderPipelineInstance(); } } public class BasicRenderPipelineInstance : RenderPipeline { public override void Render(ScriptableRenderContext renderContext, Camera[] cameras) { base.Render(renderContext, cameras); BasicRendering.Render(renderContext, cameras); } } public static class BasicRendering { // Main entry point for our scriptable render loop public static void Render(ScriptableRenderContext context, IEnumerable<Camera> cameras) { foreach (var camera in cameras) { // Culling CullingParameters cullingParams; if (!CullResults.GetCullingParameters(camera, out cullingParams)) continue; CullResults cull = CullResults.Cull(ref cullingParams, context); // Setup camera for rendering (sets render target, view/projection matrices and other // per-camera built-in shader variables). context.SetupCameraProperties(camera); // clear depth buffer var cmd = new CommandBuffer(); cmd.ClearRenderTarget(true, false, Color.black); context.ExecuteCommandBuffer(cmd); cmd.Release(); // Setup global lighting shader variables SetupLightShaderVariables(cull.visibleLights, context); // Draw opaque objects using BasicPass shader pass var settings = new DrawRendererSettings(cull, camera, new ShaderPassName("BasicPass")); settings.sorting.flags = SortFlags.CommonOpaque; settings.inputFilter.SetQueuesOpaque(); context.DrawRenderers(ref settings); // Draw skybox context.DrawSkybox(camera); // Draw transparent objects using BasicPass shader pass settings.sorting.flags = SortFlags.CommonTransparent; settings.inputFilter.SetQueuesTransparent(); context.DrawRenderers(ref settings); context.Submit(); } } // Setup lighting variables for shader to use private static void SetupLightShaderVariables(VisibleLight[] lights, ScriptableRenderContext context) { // We only support up to 8 visible lights here. More complex approaches would // be doing some sort of per-object light setups, but here we go for simplest possible // approach. const int kMaxLights = 8; // Just take first 8 lights. Possible improvements: sort lights by intensity or distance // to the viewer, so that "most important" lights in the scene are picked, and not the 8 // that happened to be first. int lightCount = Mathf.Min(lights.Length, kMaxLights); // Prepare light data Vector4[] lightColors = new Vector4[kMaxLights]; Vector4[] lightPositions = new Vector4[kMaxLights]; Vector4[] lightSpotDirections = new Vector4[kMaxLights]; Vector4[] lightAtten = new Vector4[kMaxLights]; for (var i = 0; i < lightCount; ++i) { VisibleLight light = lights[i]; lightColors[i] = light.finalColor; if (light.lightType == LightType.Directional) { // light position for directional lights is: (-direction, 0) var dir = light.localToWorld.GetColumn(2); lightPositions[i] = new Vector4(-dir.x, -dir.y, -dir.z, 0); } else { // light position for point/spot lights is: (position, 1) var pos = light.localToWorld.GetColumn(3); lightPositions[i] = new Vector4(pos.x, pos.y, pos.z, 1); } // attenuation set in a way where distance attenuation can be computed: // float lengthSq = dot(toLight, toLight); // float atten = 1.0 / (1.0 + lengthSq * LightAtten[i].z); // and spot cone attenuation: // float rho = max (0, dot(normalize(toLight), SpotDirection[i].xyz)); // float spotAtt = (rho - LightAtten[i].x) * LightAtten[i].y; // spotAtt = saturate(spotAtt); // and the above works for all light types, i.e. spot light code works out // to correct math for point & directional lights as well. float rangeSq = light.range * light.range; float quadAtten = (light.lightType == LightType.Directional) ? 0.0f : 25.0f / rangeSq; // spot direction & attenuation if (light.lightType == LightType.Spot) { var dir = light.localToWorld.GetColumn(2); lightSpotDirections[i] = new Vector4(-dir.x, -dir.y, -dir.z, 0); float radAngle = Mathf.Deg2Rad * light.spotAngle; float cosTheta = Mathf.Cos(radAngle * 0.25f); float cosPhi = Mathf.Cos(radAngle * 0.5f); float cosDiff = cosTheta - cosPhi; lightAtten[i] = new Vector4(cosPhi, (cosDiff != 0.0f) ? 1.0f / cosDiff : 1.0f, quadAtten, rangeSq); } else { // non-spot light lightSpotDirections[i] = new Vector4(0, 0, 1, 0); lightAtten[i] = new Vector4(-1, 1, quadAtten, rangeSq); } } // ambient lighting spherical harmonics values const int kSHCoefficients = 7; Vector4[] shConstants = new Vector4[kSHCoefficients]; SphericalHarmonicsL2 ambientSH = RenderSettings.ambientProbe * RenderSettings.ambientIntensity; GetShaderConstantsFromNormalizedSH(ref ambientSH, shConstants); // setup global shader variables to contain all the data computed above CommandBuffer cmd = new CommandBuffer(); cmd.SetGlobalVectorArray("globalLightColor", lightColors); cmd.SetGlobalVectorArray("globalLightPos", lightPositions); cmd.SetGlobalVectorArray("globalLightSpotDir", lightSpotDirections); cmd.SetGlobalVectorArray("globalLightAtten", lightAtten); cmd.SetGlobalVector("globalLightCount", new Vector4(lightCount, 0, 0, 0)); cmd.SetGlobalVectorArray("globalSH", shConstants); context.ExecuteCommandBuffer(cmd); cmd.Dispose(); } // Prepare L2 spherical harmonics values for efficient evaluation in a shader private static void GetShaderConstantsFromNormalizedSH(ref SphericalHarmonicsL2 ambientProbe, Vector4[] outCoefficients) { for (int channelIdx = 0; channelIdx < 3; ++channelIdx) { // Constant + Linear // In the shader we multiply the normal is not swizzled, so it's normal.xyz. // Swizzle the coefficients to be in { x, y, z, DC } order. outCoefficients[channelIdx].x = ambientProbe[channelIdx, 3]; outCoefficients[channelIdx].y = ambientProbe[channelIdx, 1]; outCoefficients[channelIdx].z = ambientProbe[channelIdx, 2]; outCoefficients[channelIdx].w = ambientProbe[channelIdx, 0] - ambientProbe[channelIdx, 6]; // Quadratic polynomials outCoefficients[channelIdx + 3].x = ambientProbe[channelIdx, 4]; outCoefficients[channelIdx + 3].y = ambientProbe[channelIdx, 5]; outCoefficients[channelIdx + 3].z = ambientProbe[channelIdx, 6] * 3.0f; outCoefficients[channelIdx + 3].w = ambientProbe[channelIdx, 7]; } // Final quadratic polynomial outCoefficients[6].x = ambientProbe[0, 8]; outCoefficients[6].y = ambientProbe[1, 8]; outCoefficients[6].z = ambientProbe[2, 8]; outCoefficients[6].w = 1.0f; } }
RenderPipelineクラスも必要
using System; namespace UnityEngine.Experimental.Rendering { public abstract class RenderPipeline : IRenderPipeline { public virtual void Render(ScriptableRenderContext renderContext, Camera[] cameras) { if (disposed) throw new ObjectDisposedException(string.Format("{0} has been disposed. Do not call Render on disposed RenderLoops.", this)); } public bool disposed { get; private set; } public virtual void Dispose() { disposed = true; } } }
Assets/BasicRenderPipelineTutorial/BasicRenderPipeline.asset 等の名前とフォルダは矛盾が無いようにしよう
Editorメニューから CreateAssetを行うようになっている
Asset作成
Editorメニューより RenderPipeline->CreateBasicRenderPipeline (上記のコード変更してたら その名前)で Assetを作成する 成功すれば BasicRenderPipeline.asset が作成されている Scriptプロパティには 上記のスクリプト名が入っている 何か問題があれば、ファイル名やフォルダが間違えていると思われる
アタッチ
メニューより Edit->ProjectSettings->Graphic ScriptableRendererLoopSettings に先ほど作った BasicRenderPipeline アセットを指定 すると RenderPipelineをカスタマイズできる
多分 シェーダーとか、Pipelineを変えないとまともに描画できないと思うけど
右辺値参照~完全転送まで100%理解するぞ! part8 おまけ。
おまけ
可変引数テンプレートの Parameter Bagを展開しながら 全部出力する
#include<iostream> using namespace std; struct Concat{ template <class... T> void operator ()(T&&... t) { concat_(std::forward<T>(t)...); } private: void concat_() { } template <class Head, class... Tail> void concat_(Head&& head, Tail&&... tail) { cout << head; concat_(std::forward<Tail>(tail)...); } }; auto main() -> int { Concat con; con("test int:", 1 ); return 1; }
上記のように
template
今はcoutに出力しているが 当然 operator << を実装したくなる
#include<iostream> #include<sstream> using namespace std; struct Concat{ template <class... T> Concat& operator ()(T&&... t) { concat_(std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream &os, Concat& obj); private: stringstream ss_; void concat_() { } template <class Head, class... Tail> void concat_(Head&& head, Tail&&... tail) { ss_ << head; concat_(std::forward<Tail>(tail)...); } }; inline ostream& operator << (ostream &os, Concat& obj){ os << obj.ss_.str(); obj.ss_.str(""); return os; } auto main() -> int { Concat con; cout << con("test int:", 1 ) << endl; return 1; }
operator() で thisを返し operator << にて streamへ。
最期に 再帰の部分をフリー関数へ(私の好み)
#include<iostream> #include<sstream> using namespace std; ostream& concat_( ostream &ss_) { return ss_; } template <class Head, class... Tail> ostream& concat_(ostream &ss_, Head&& head, Tail&&... tail) { ss_ << head; return concat_(ss_, std::forward<Tail>(tail)...); } struct Concat{ template <class... T> Concat& operator ()(T&&... t) { concat_(ss_, std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream &os, Concat& obj); private: stringstream ss_; }; inline ostream& operator << (ostream &os, Concat& obj){ os << obj.ss_.str(); obj.ss_.str(""); return os; } auto main() -> int { Concat con; cout << con("test int:", 1 ) << endl; return 1; }
右辺値参照~完全転送まで100%理解するぞ! part7 可変引数テンプレート
前回の
template<class T> void hoge(T&& x){ std::forward<T>(x); }
universal referenceを使うことで、右辺値参照も左辺値参照もどちらも入ってくる
std::forwardを使うことで、右辺値参照の場合は ムーブコンストラクタ、左辺値参照の場合は コピーコンストラクタ が実行される
これでほぼ完ぺきだけど 完全転送と呼ぶには hoge(x,y,z...) と、可変引数に対応しなければいけません!
可変引数テンプレート
まずは可変引数テンプレートの説明
可変引数(printfのような va_list)のような書式で コンパイル時に展開される
#include<iostream> using namespace std; void bar( int i, string& s ){ cout << i << s << endl; }; template<class... T> void hoge(T... t){ bar(t...); } auto main() -> int { hoge(1, string(" test") ); // hoge(1); // hoge(int) は宣言されていないので エラー return 1; }
上記コードの template<class... T> で、0個以上の引数をもつものにヒットする bar(t...); で、tを展開したパラメーターを引数にもつ barを呼ぶ
barに必要なだけオーバーロードすればいい
あとはこれを universal reference と std::forwardを使い
#include<iostream> using namespace std; void bar( int i, string s ){ cout << i << s << endl; }; template<class... T> void hoge(T&&... t){ bar(std::forward<T>(t)...); } auto main() -> int { string s("s"); hoge(1, s ); hoge(1, " test" ); // hoge(1); // hoge(int) は宣言されていないので エラー return 1; }
これで、どんな引数でも完全転送が出来ました!
ま、ビットフィールド等 工夫しなければ完全転送できないパターンもあるようですが 一般的には これでOK
クラスのコンストラクタを可変長テンプレートにして ごにょごにょ
前回なやんだ続きではあります
関数テンプレートは暗黙的インスタンス化可能だが、クラステンプレートは不可能 ならば コンストラクタを可変長テンプレートすればいいじゃない
#include<iostream> #include<sstream> using namespace std; struct Hoge{ void stats_(){}; stringstream ss_; template <class Head, class... Tail> void stats_(Head&& head, Tail&&... tail){ ss_ << head; stats_(std::forward<Tail>(tail)...); } template <class... T> Hoge (T&&... t){ stats_(std::forward<T>(t)...); } friend ostream& operator << (ostream& os, const Hoge& hoge); }; ostream& operator << (ostream& os, const Hoge& hoge){ os << hoge.ss_.str(); return os; } int main() { cout << Hoge("aaa,", "bbb,", "ccc,") << endl; }
つまり 関数オブジェクトは
#include<iostream> #include<sstream> using namespace std; struct Hoge{ void stats_(){}; stringstream ss_; template <class Head, class... Tail> void stats_(Head&& head, Tail&&... tail){ // std::cout << head; ss_ << head; stats_(std::forward<Tail>(tail)...); } template <class... T> Hoge& operator() (T&&... t){ stats_(std::forward<T>(t)...); return *this; } friend ostream& operator << (ostream& os, const Hoge& hoge); }; ostream& operator << (ostream& os, const Hoge& hoge){ os << hoge.ss_.str(); return os; } int main() { cout << Hoge()("aaa,", "bbb,", "ccc,") << endl; }
こうやればできたんだなーー
関数オブジェクトでは 自分を返して汎用的にしてみたけど、何を返すのが正しいのかは わからない