Destiny Shader Pipeline(GDC 2017) -TFX Shader System(1)
引言
隨著Natalya Tatarchuk 從2017年開始在Unity擔任Graphics Director,到Unity 2018.1 發佈Shader Graph features,讓小編想要好好複習娜姐之前在Bungie時的《Destiny》GDC系列講座。
最早在2015年時就已經將Data-driven跟Components概念實作在為Destiny系列作研發的自製引擎Multithreaded Rendering Architecture中,到2017年更發展出Destiny shader pipeline中的核心系統 — TFX shader system。這個架構的彈性讓Bungie能夠在Destiny Open world的設定中應付不同地形、天候的render效果,也讓Bungie公司能夠以極少數的graphics programmer應付數以百計的artists需求。
TFX Shader System
TFX Source
TFX Source讓graphics programmer或TA能使用TFX語言定義HLSL的nodes, parameters和fragments。
Baking
在baking階段,將shaders編譯分為三個步驟:
- 將TFX Source定義的HLSL fragments 拼接在一起來生成HLSL。
- pass給平台的shader compiler
- 將結果與在node graph或TFX source定義的參數值做關聯。
Runtime Technique
在runtime時將最終結果submit給GPU。
這流程到底是如何運作的呢?文章中提供了一個簡單的範例:
import “main_vs.tfx”
import “interpolators.tfx” #hlsl
float4 main_ps(s_ps_in ps_in) : TEX_TARGET0
{
float2 transformed_texcoord=
frac (ps_in.texcoord * 4.0f + 0.5f);
return float4(transformed_texcoord, o.of, 1.0f);
}
#end technique my_technique
{
compile_shader(all_platforms, vs, main_vs);
compile_shader(all_platforms, ps, main_ps);
}
首先,“import” vertex shader跟interpolators。
在#hlsl #end之間這一部分是一個HLSL FRAGMENT,當進行splicing時會被copy/pasted到生成的HLSL中。
而這些 HLSL FRAGMENT實際上是跨平台的並用pre processor來定義。例如TFX_TARGET0,定義為SV_TARGET0(HLSL)或COLOR0或S_TARGET_OUTPUT0(PSSL)取決於baking平台。
下方的technique定義了如何在baking階段中建構HLSL。基本上它是說:如果有人要求’my_technique’,那麼就為所有平台編譯vertex shader和pixel shader,並使用’main_vs’和’main_ps’當入口點。
編譯出來的HLSL結果如下:
// entry_point: main_ps shader: ps_5_0 // global_d3d11. tfx
#define TFX_POSITION SV_Position
#define TFX_TEXCOORD0 TEXCOORD0
#define TFX TARGET0 SV_Target0 // interpolators.tfx
struct s_ps_in
{
float4 pos : TEX_POSITION;
float2 texcoord : TEX_TEXCOORDO;
}; // example.tfx
float4 main_ps(s_ps_in ps_in) : TFX_TARGET0
{
float2 transformed texcoord=
frac (ps_in.texcoord * 4.0f + 0.5f);
return float4(transformed_texcoord, 0.0f, 1.0f);
}
接下來,加入控制參數吧!這部分就可以看出如何做到在node graph editor中加入參數node,實作方式彷彿看到熟悉的c++ class呢!
Data-Driven Shader Parameters
Data-Driven的設計能讓加入shader參數時,不需要重新rebuild,也能在Node Graph UI、Baking state或Runtime都能自動正確取得參數值,真是個聰明的用法。
// artist parameters for scale/offset transform
float2 scale @default(float2(1.0f, 1.0f));
float2 center @default(float2(0.5f, 0.5f));
float2 offset @default(float2(0.of, 0.0f));#hlsl
float4 main_ps(s_ps_in ps_in) : TFX_TARGETO
{
float4 xform= float4(
scale,
offset + center — center * scale);
float2 transformed_texcoord=
frac(ps_in.texcoord * xform.xy + xform.zw); return float4(transformed_texcoord, o.of, 1.0f);
}
#end
當加入’scale’,’center’和’offset’這三個參數來控制transform,並設計讓artist調整時,data-driven的設計能讓接口輕易的跟Node Graph UI結合,Artist能在編輯器中改變參數值。
在TFX metadata system中,以@符號開頭的都是meta-data tag。meta-data system的設計是非常實用的,當有新的shader language features,能輕易的接入,否則可能要針對新features而回去重寫或新增功能呢!
但是,事情不會永遠這麼單純的只控制transform,所以,TFX Components又產生啦!
TFX Components
我們可以把Component想像成C++中的classes,有以下特點:
- 把member variables當成member parameters
- 把member functions 當成member HLSL fragments
- 可以被instantiate。
有了Components的設計,每個node graphic node可以被Component定義,大大地提供了彈性,也能被重複使用,也能善用界面特性,將複雜部分或平台差異包裝隱藏起來。
// interface: c_transform
// variant: scale_offset
component c_transform:scale_offset
{
// member parameters
float2 scale @default(float2(1.0f, 1.0f));
float2 center @default(float2(0.0f, 0.0f));
float2 offset @default(float2(0.of, 0.0f));
// member HLSL function ‘apply
#function(apply)
float2 apply(float2 texcoord)
{
return texcoord * scale +
offset + center * (1.0f — scale);
}
#end
}
這個Component使用’c_transform’ interface,variant為’scale_offset’。接下來將三個參數當成member parameters,並有個member HLSL function ’apply’,所有使用c_transform的都Component應該有“apply”函數。
import “main_vs.tfx”
import “interpolators.tfx”
import “transform_components.tfx”
// instance of c_transform:scale_offset
c_transform:scale_offset g_transform; #hlsl
float4 main_ps(s_ps_in ps_in) : TEX_TARGET0
{
float2 transformed_texcoord=
g_transform.apply(ps_in.texcoord);
return float4( frac(transformed_texcoord), 0.0f, 1.0f);
}
#end
回到原來的shader code,instantiate c_transform,像使用member value跟member function一樣,就可以做到inline component的所有members到node graph中喔~
import “main_vs.tfx”
import “interpolators.tfx”
import “transform_components.tfx”// instance of c_transform: none
component c_transform:* g_transform @default(none); #hlsl
float4 main_ps (s_ps_in ps_in) : TFX_TARGET0
{
float2 transformed_texcoord=
g_transform.apply(ps_in.texcoord);
return float4( frac(transformed_texcoord), 0.0f, 1.0f);
}
#end
也能夠Instantiate Component interface:*,讓node接口更多元。
是不是覺得其實概念想得到,只是要如何實作到系統裡,必須要層層的互相支援,就先介紹到這邊喔!
Reference
http://advances.realtimerendering.com/destiny/gdc_2017/index.html