Destiny Shader Pipeline(GDC 2017) -TFX Shader System(1)

Annie Chen
Akatsuki Taiwan Technology
10 min readJul 23, 2019

引言

隨著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編譯分為三個步驟:

  1. 將TFX Source定義的HLSL fragments 拼接在一起來生成HLSL。
  2. pass給平台的shader compiler
  3. 將結果與在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);
}
這個簡單的範例其實只是對texture coordinates做內插縮放並將它們寫出到render target,如上圖。

首先,“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,有以下特點:

  1. 把member variables當成member parameters
  2. 把member functions 當成member HLSL fragments
  3. 可以被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中喔~

Node
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

--

--