2 前向渲染
前向渲染是三個(gè)光照技術(shù)中最簡(jiǎn)單的,也是游戲圖形渲染中最常見(jiàn)的技術(shù)焕蹄。出于這個(gè)原因逾雄,也是光照計(jì)算最昂貴的技術(shù),它不允許在場(chǎng)景中出現(xiàn)大量的動(dòng)態(tài)光源腻脏。
大部分使用前向渲染的圖形引擎會(huì)采用一些技術(shù)來(lái)模擬場(chǎng)景中大量的光源的情況鸦泳,例如,lightmap(光照貼圖)和lightProbe(light probe)都是采用從場(chǎng)景中放置的靜態(tài)光源預(yù)先計(jì)算光照貢獻(xiàn)的方法永品,并將這些光照貢獻(xiàn)存儲(chǔ)在紋理中做鹰,以便在運(yùn)行時(shí)加載。不幸的是鼎姐,lightmap和lightprobe不能模擬場(chǎng)景中的動(dòng)態(tài)光源钾麸,因?yàn)檫@些光源產(chǎn)生的光照貼圖常常在運(yùn)行時(shí)會(huì)被廢棄掉(discard)。
在這個(gè)實(shí)驗(yàn)中炕桨,前向渲染的結(jié)果被用作與另外兩個(gè)渲染技術(shù)進(jìn)行對(duì)比的基準(zhǔn)饭尝。前向渲染技術(shù)也被用來(lái)構(gòu)建與其它渲染技術(shù)進(jìn)行性能對(duì)比的基準(zhǔn)(baseline)。
很多在前向渲染中的方法會(huì)在延遲和forward+中被復(fù)用献宫,例如芋肠,前向渲染中的頂點(diǎn)著色器也會(huì)被用在延遲渲染和forward+渲染中。同樣遵蚜,計(jì)算最終光照和材質(zhì)著色的方法也被用于所有的渲染技術(shù)帖池。
在下一部分奈惑,我會(huì)描述前向渲染的實(shí)現(xiàn)細(xì)節(jié)。
2.1 頂點(diǎn)著色器(vertex shader)
vertex shader對(duì)所有的渲染技術(shù)是通用的睡汹,在這個(gè)實(shí)驗(yàn)中肴甸,只支持靜態(tài)幾何體,沒(méi)有骨骼動(dòng)畫(huà)和地表囚巴,這些需要不同的vertex shader原在。 vertex shader盡可能簡(jiǎn)單到可以支持pixel shader中的一些功能,如法線映射(normal mapping)彤叉。
在展示vertex shader的代碼之前庶柿,我會(huì)描述一下vertex shader使用的數(shù)據(jù)結(jié)構(gòu)。
// CommonInclude.hlsl
140 struct AppData
141 {
142 float3 position : POSITION;
143 float3 tangent : TANGENT;
144 float3 binormal : BINORMAL;
145 float3 normal : NORMAL;
146 float2 texCoord : TEXCOORD0;
147 };
AppData這個(gè)結(jié)構(gòu)定義的需要被應(yīng)用程序代碼發(fā)送到GPU端的數(shù)據(jù)秽浇。除了用于法線映射的normal向量浮庐,我們也需要發(fā)送切線(tagent)向量,副法線(或副切線)向量是可選的柬焕。切線和副法線即可以由3D美術(shù)師創(chuàng)建模型時(shí)生成审残,也可以由模型加載器進(jìn)行生成。在本例中斑举,如果它們沒(méi)有被3D美術(shù)師生成搅轿,我則使用Open Asset Import Library[7]來(lái)生成切線和副切線。
在vertex shader中富玷,我們也需要知道如何將模型空間向量變換成Pixel shader中需要的視圖空間(view space)向量璧坟,為了實(shí)現(xiàn)這個(gè)變換,我們需要發(fā)送world, view以及投影(projection)矩陣到vertex shader赎懦。為了存儲(chǔ)這些vertex shader中需要的每個(gè)模型的變量沸柔,我會(huì)創(chuàng)建一個(gè)常量緩沖區(qū)(constant buffer)。
// CommonInclude.hlsl
149 cbuffer PerObject : register( b0 )
150 {
151 float4x4 ModelViewProjection;
152 float4x4 ModelView;
153 };
因?yàn)槲也恍枰獑为?dú)存儲(chǔ)世界矩陣铲敛,因此我在應(yīng)用程序中預(yù)計(jì)算組合的model和view矩陣褐澎,以及組合的model、view和projection矩陣伐蒋,為vertex shader將它們發(fā)送到一個(gè)單獨(dú)的常量緩沖區(qū)中工三。
vertex shader的輸出(也即是pixel shader的輸入)看起來(lái)是這樣的:
CommonInclude.hlsl
181 struct VertexShaderOutput
182 {
183 float3 positionVS : TEXCOORD0; // View space position.
184 float2 texCoord : TEXCOORD1; // Texture coordinate
185 float3 tangentVS : TANGENT; // View space tangent.
186 float3 binormalVS : BINORMAL; // View space binormal.
187 float3 normalVS : NORMAL; // View space normal.
188 float4 position : SV_POSITION; // Clip space position.
189 };
VertexShaderOutput結(jié)構(gòu)用來(lái)傳遞變換過(guò)的頂點(diǎn)屬性(vertex attribute)到Pixel shader,vs
后綴的成員表示該向量是view空間的先鱼。我選擇在view空間做所有的光照俭正,而不是在世界空間,這是因?yàn)樵趯?shí)現(xiàn)延遲渲染和Forward+渲染時(shí)在view空間坐標(biāo)下更容易焙畔。
vertex shader是很直接而簡(jiǎn)短的掸读,它唯一的目標(biāo)是將應(yīng)用程序傳來(lái)的模型空間向量變換成pxiel shader中使用的view空間向量。
vertex shader也必須要計(jì)算出光柵器(rasterizer)需要的在裁剪空間(clip space)中的position,用于vertex shader輸出的SV_POSITION被用于裁剪空間的位置儿惫,但該語(yǔ)義也可以作為pixel shader的輸入變量澡罚。當(dāng)SV_POSITION被用作pixel shader的輸入時(shí),該值表示屏幕空間(screen space)的位置[8]肾请,在延遲渲染和forward+的shader中留搔,我會(huì)使用該語(yǔ)義來(lái)獲取當(dāng)前像素在屏幕空間的位置。
// ForwardRendering.hlsl
3 VertexShaderOutput VS_main( AppData IN )
4 {
5 VertexShaderOutput OUT;
6
7 OUT.position = mul( ModelViewProjection, float4( IN.position, 1.0f ) );
8
9 OUT.positionVS = mul( ModelView, float4( IN.position, 1.0f ) ).xyz;
10 OUT.tangentVS = mul( ( float3x3 )ModelView, IN.tangent );
11 OUT.binormalVS = mul( ( float3x3 )ModelView, IN.binormal );
12 OUT.normalVS = mul( ( float3x3 )ModelView, IN.normal );
13
14 OUT.texCoord = IN.texCoord;
15
16 return OUT;
17 }
你會(huì)注意到我會(huì)使用矩陣乘以輸入向量(矩陣在前铛铁,向量在后)隔显,這意味著矩陣是以主列(column-major)順序進(jìn)行存儲(chǔ)的。DirectX 10之前饵逐,HLSL中的矩陣是以主行(row-major)的順序進(jìn)行加載的括眠,輸入的向量是后乘矩陣的(向量在前,矩陣在后)倍权,DirectX 10之后掷豺,矩陣默認(rèn)加載的是主列順序。你可以通過(guò)在矩陣的聲明處指定主行修飾符來(lái)改變默認(rèn)順序[9]账锹。
2.2 像素著色器(Pixel Shader)
pixel shader會(huì)計(jì)算所有的光照和著色萌业,用于決定一個(gè)屏幕像素的最終顏色坷襟。在Pixel shader中采用的光照方程參考DirectX 11中的紋理和光照奸柬,如果你對(duì)光照方程不熟悉的話,在繼續(xù)之前需要去閱讀這篇文章婴程。
pixel shader使用幾個(gè)結(jié)構(gòu)來(lái)做這項(xiàng)工作廓奕,Material結(jié)構(gòu)存儲(chǔ)了描述被著色對(duì)象表面材質(zhì)的所有信息,Light struct包含了描述場(chǎng)景燈光的所有參數(shù)档叔。
2.2.1 材質(zhì)(Material)
Material定義了用于描述當(dāng)前著色對(duì)象表面的所有屬性桌粉,因?yàn)橐恍┎馁|(zhì)屬性可能還有相關(guān)的紋理(如,diffuse紋理衙四,specular紋理铃肯,或者法線貼圖),我們也會(huì)使用材質(zhì)來(lái)指明這個(gè)紋理是否呈現(xiàn)在這個(gè)對(duì)象上传蹈。
// CommonInclude.hlsl
10 struct Material
11 {
12 float4 GlobalAmbient;
13 //-------------------------- ( 16 bytes )
14 float4 AmbientColor;
15 //-------------------------- ( 16 bytes )
16 float4 EmissiveColor;
17 //-------------------------- ( 16 bytes )
18 float4 DiffuseColor;
19 //-------------------------- ( 16 bytes )
20 float4 SpecularColor;
21 //-------------------------- ( 16 bytes )
22 // Reflective value.
23 float4 Reflectance;
24 //-------------------------- ( 16 bytes )
25 float Opacity;
26 float SpecularPower;
27 // For transparent materials, IOR > 0.
28 float IndexOfRefraction;
29 bool HasAmbientTexture;
30 //-------------------------- ( 16 bytes )
31 bool HasEmissiveTexture;
32 bool HasDiffuseTexture;
33 bool HasSpecularTexture;
34 bool HasSpecularPowerTexture;
35 //-------------------------- ( 16 bytes )
36 bool HasNormalTexture;
37 bool HasBumpTexture;
38 bool HasOpacityTexture;
39 float BumpIntensity;
40 //-------------------------- ( 16 bytes )
41 float SpecularScale;
42 float AlphaThreshold;
43 float2 Padding;
44 //--------------------------- ( 16 bytes )
45 }; //--------------------------- ( 16 * 10 = 160 bytes )
GlobalAmbient用來(lái)描述全局地作用于所有對(duì)象上的環(huán)境光屬性押逼,從技術(shù)上而言,該變量應(yīng)當(dāng)是一個(gè)全局變量(不指定到單一對(duì)象)惦界,但因?yàn)樵谝粋€(gè)pixel shader一次只有一個(gè)材質(zhì)挑格,因此我認(rèn)為這是一個(gè)比較好的位置來(lái)存儲(chǔ)它。
ambient, emissive, diffuse和specular顏色與在DirectX 11中的紋理和光照中具有相同的意義沾歪,所以這里不再進(jìn)一步解釋漂彤。
Reflectance用來(lái)表示應(yīng)當(dāng)與diffuse顏色混合的反射顏色的數(shù)量,這需要環(huán)境貼圖(cube texture)來(lái)實(shí)現(xiàn),在該實(shí)驗(yàn)中不會(huì)用到挫望。
Opacity用來(lái)決定一個(gè)對(duì)象的總的不透明度立润,這個(gè)值可以用來(lái)讓物體顯示透明,該屬性用來(lái)在透明pass中渲染半透明物體士骤,如果該值小于1(1表示完全不透明范删,0表示完全透明),該物體會(huì)被認(rèn)為是透明的拷肌,將會(huì)在透明Pass中渲染這個(gè)物體到旦,而不是在opaque pass中。
變量SpecularPower用來(lái)決定對(duì)象看起來(lái)有多閃亮巨缘,在DirectX 11中的紋理和光照中有對(duì)該變量的詳細(xì)解釋添忘。
在29-38行定義的變量HasTexture指明該對(duì)象是否使用相關(guān)的紋理進(jìn)行渲染,如果該參數(shù)為true若锁,相應(yīng)的紋理會(huì)被采樣搁骑,采樣得到的紋素(texel, 與pixel進(jìn)行區(qū)分)會(huì)與相應(yīng)的材質(zhì)顏色進(jìn)行混合。
BumpIntensity被用來(lái)縮放從bump貼圖中得到的高度值(不要與法線映射混淆又固,法線不會(huì)進(jìn)行縮放)仲器,以此來(lái)平滑(soften)或強(qiáng)化物體表面的起伏。大多數(shù)情況下仰冠,模型會(huì)使用法線貼圖來(lái)增加沒(méi)有細(xì)分(tessellation)的物體表面的細(xì)節(jié)乏冀,但也可以使用高度圖(heightmap)來(lái)做同樣的事情。如果模型使用了bump貼圖洋只,材質(zhì)的HasBumpTexture屬性會(huì)被設(shè)置為true辆沦,這種情況下模型使用被bump映射而不是法線映射。
SpecularScale用來(lái)縮放從高光強(qiáng)度紋理中讀取的高光強(qiáng)度值(specular power value)识虚。因?yàn)榧y理通常保存無(wú)符號(hào)的歸一化(normalized)的值肢扯,從紋理中采樣的值被讀取為[0..1]范圍的浮點(diǎn)數(shù)。1.0的高光強(qiáng)度沒(méi)有意義担锤,所以從紋理中讀取的高光強(qiáng)度在參與最終的光照計(jì)算之前會(huì)被SpecularScale進(jìn)行縮放蔚晨。
AlphaThreshold用來(lái)丟棄不透明度低于某個(gè)值的像素,通常在pixel shader中使用"discard"肛循。這可以被用于"cut-out"材質(zhì)铭腕,使用該材質(zhì)的物體不需要alpha進(jìn)行blend,但在物體上卻有洞(例如鏈接的柵欄)育拨。
Padding用來(lái)顯式的增加8個(gè)字節(jié)來(lái)填充material結(jié)構(gòu)谨履。盡管HLSL會(huì)隱式上增加這個(gè)填充(8個(gè)字節(jié))到該結(jié)構(gòu),以確保該結(jié)構(gòu)是16字節(jié)的倍數(shù)熬丧,顯式的增加填充會(huì)更加明確該結(jié)構(gòu)的尺寸和對(duì)齊方式與相應(yīng)的C++副本一致笋粟。
材質(zhì)屬性通過(guò)一個(gè)常量緩沖區(qū)傳遞給pixel shader怀挠。
// CommonInclude.hlsl
155 cbuffer Material : register( b2 )
156 {
157 Material Mat;
158 };
常量緩沖區(qū)與buffer寄存器的slot分配被用于該本文的所有pixel shader。
2.2.2 紋理
材質(zhì)已經(jīng)支持了8種不同類型的紋理
- 環(huán)境貼圖-Ambient
- 自發(fā)光貼圖-Emissive
- 漫反射貼圖-Diffuse
- 高光貼圖-Specular
- 高光強(qiáng)度貼圖-SpecularPower
- 法線貼圖-Normals
- 凹凸貼圖-Bump
- 不透明度貼圖-Opacity
并非所有的場(chǎng)景對(duì)象會(huì)用到所有的紋理插槽(slot)(法線貼圖和bump貼圖是互斥的害捕,所以它們可能可以復(fù)用同一個(gè)紋理插槽)绿淋,這取決于3D美術(shù)師讓場(chǎng)景中的模型使用哪些紋理。應(yīng)用程序會(huì)加載一個(gè)材質(zhì)相關(guān)的紋理尝盼,一個(gè)紋理參數(shù)和一個(gè)相關(guān)的紋理插槽為每個(gè)這些材質(zhì)屬性而聲明吞滞。
// CommonInclude.hlsl
167 Texture2D AmbientTexture : register( t0 );
168 Texture2D EmissiveTexture : register( t1 );
169 Texture2D DiffuseTexture : register( t2 );
170 Texture2D SpecularTexture : register( t3 );
171 Texture2D SpecularPowerTexture : register( t4 );
172 Texture2D NormalTexture : register( t5 );
173 Texture2D BumpTexture : register( t6 );
174 Texture2D OpacityTexture : register( t7 );
在本文的每個(gè)pixel shader中,紋理插槽0-7為這些紋理而保留盾沫。
2.2.3 燈光
Light結(jié)構(gòu)存儲(chǔ)了場(chǎng)景中定義一個(gè)燈光所需的所有信息裁赠。聚光燈,點(diǎn)光源和方向光沒(méi)有分開(kāi)到不同的結(jié)構(gòu)中赴精,定義任意一種類型的燈光所有必須的屬性都存儲(chǔ)在一個(gè)結(jié)構(gòu)中佩捞。
CommonInclude.hlsl
47 struct Light
48 {
49 /**
50 * Position for point and spot lights (World space).
51 */
52 float4 PositionWS;
53 //--------------------------------------------------------------( 16 bytes )
54 /**
55 * Direction for spot and directional lights (World space).
56 */
57 float4 DirectionWS;
58 //--------------------------------------------------------------( 16 bytes )
59 /**
60 * Position for point and spot lights (View space).
61 */
62 float4 PositionVS;
63 //--------------------------------------------------------------( 16 bytes )
64 /**
65 * Direction for spot and directional lights (View space).
66 */
67 float4 DirectionVS;
68 //--------------------------------------------------------------( 16 bytes )
69 /**
70 * Color of the light. Diffuse and specular colors are not seperated.
71 */
72 float4 Color;
73 //--------------------------------------------------------------( 16 bytes )
74 /**
75 * The half angle of the spotlight cone.
76 */
77 float SpotlightAngle;
78 /**
79 * The range of the light.
80 */
81 float Range;
82
83 /**
84 * The intensity of the light.
85 */
86 float Intensity;
87
88 /**
89 * Disable or enable the light.
90 */
91 bool Enabled;
92 //--------------------------------------------------------------( 16 bytes )
93
94 /**
95 * Is the light selected in the editor?
96 */
97 bool Selected;
98
99 /**
100 * The type of the light.
101 */
102 uint Type;
103 float2 Padding;
104 //--------------------------------------------------------------( 16 bytes )
105 //--------------------------------------------------------------( 16 * 7 = 112 bytes )
106 };
Position和Direction同時(shí)存儲(chǔ)了世界空間(WS后綴)和視圖空間(VS后綴)中的位置和方向。當(dāng)然蕾哟,位置屬性只應(yīng)用于點(diǎn)光源和聚光燈一忱,方向?qū)傩灾蛔饔糜诰酃鉄艉头较蚬狻V煌瑫r(shí)存儲(chǔ)了兩個(gè)不同空間谭确,是因?yàn)樵趹?yīng)用程序階段世界空間更易于使用帘营,然后在傳遞給GPU之前將世界空間轉(zhuǎn)換成視圖空間,使用這種方式可以不再需要多余的GPU存儲(chǔ)空間來(lái)管理多個(gè)燈光列表逐哈。因?yàn)?0,000燈光才只需要1.12MB的GPU內(nèi)存芬迄,所以這是一個(gè)合理的犧牲。但是最小化燈光結(jié)構(gòu)對(duì)GPU緩存有積極的一面鞠眉,并能提高渲染性能薯鼠。
在一些光照模型中择诈,漫反射和高光光照貢獻(xiàn)是分開(kāi)的械蹋,因?yàn)檫@種差異很小,這里選擇不分開(kāi)兩者的貢獻(xiàn)羞芍,而是將兩者存儲(chǔ)在Color變量中哗戈。
SpotlightAngle是以角度來(lái)表示的聚光燈圓椎體的半角,使用角度比弧度(radian)更加直觀荷科。當(dāng)然聚光燈的角度會(huì)在shader中計(jì)算余弦(consine)時(shí)被轉(zhuǎn)換成弧度唯咬。
- 圖3,聚光燈的角度
Range決定了燈光到達(dá)表面的距離畏浆,同時(shí)也決定了燈光達(dá)表面的貢獻(xiàn)胆胰。雖然在物理上不完全正確(真實(shí)的燈光有一個(gè)衰減,實(shí)際上不會(huì)是0)刻获,燈光需要有一個(gè)有限的范圍來(lái)實(shí)現(xiàn)延遲著色和Forward+渲染技術(shù)蜀涨。這個(gè)范圍的單位是場(chǎng)景特定的,但這里會(huì)使用1單位是1米的規(guī)格。對(duì)于點(diǎn)光源厚柳,范圍是代表光的球體的半徑氧枣,對(duì)于聚光燈,范圍是代表光的圓錐體的長(zhǎng)度别垮。方向光不使用范圍便监,因?yàn)樗鼈儽徽J(rèn)為是無(wú)限遠(yuǎn)的,且指向同一個(gè)方向碳想。
Intensity用于調(diào)節(jié)計(jì)算出的光貢獻(xiàn)烧董。默認(rèn)情況下,這個(gè)值是1胧奔,它可以用來(lái)調(diào)節(jié)燈的亮度解藻。
Enabled標(biāo)志可以控制場(chǎng)景中燈光的開(kāi)啟或關(guān)閉,Enabled為false的燈會(huì)在shader中被跳過(guò)葡盗。
在本demo中螟左,燈光是可以被編輯的,可以通過(guò)在demo中點(diǎn)擊一個(gè)燈來(lái)選中它觅够,它的屬性也可以被修改胶背,為了表明一個(gè)燈被選中,Selected標(biāo)記會(huì)被設(shè)置為true喘先。當(dāng)一個(gè)燈在場(chǎng)景中被選中時(shí)钳吟,它會(huì)表現(xiàn)的暗一些,以表明它被中了窘拯。
Type用來(lái)指定該燈光的類型红且,可以是下面其中之一:
// CommonInclude.hlsl
6 #define POINT_LIGHT 0
7 #define SPOT_LIGHT 1
8 #define DIRECTIONAL_LIGHT 2
再一次給Light結(jié)構(gòu)顯式地添加8個(gè)字節(jié)的填充,以匹配C++中的struct布局涤姊,并使用該結(jié)構(gòu)滿足HLSL需要的16字節(jié)對(duì)齊暇番。
燈光數(shù)組通過(guò)StructuredBuffer進(jìn)行訪問(wèn),大部分光照Shader的實(shí)現(xiàn)都會(huì)使用常量緩沖區(qū)(constant buffer)進(jìn)行存儲(chǔ)思喊,但是常量緩沖區(qū)限制64KB的大小壁酬,這也意味著在耗盡GPU上的常量?jī)?nèi)存之前最多可以使用570個(gè)動(dòng)態(tài)光源臭脓。結(jié)構(gòu)化的緩沖區(qū)(structured buffer)存儲(chǔ)在紋理內(nèi)存上爵嗅,它受限于GPU提供的可用紋理內(nèi)存數(shù)量(在桌面GPU上通常按GB來(lái)算)误墓。在大部分GPU上紋理內(nèi)存是很快的浅浮,所以使用紋理內(nèi)存存儲(chǔ)燈光不會(huì)有性能上的影響米同,事實(shí)上惫恼,在一些特定的GPU上(NVIDIA GeForce GTX 680)棚贾,將數(shù)據(jù)放在結(jié)構(gòu)化的緩沖區(qū)上反而有一定的性能提升酥宴。
// CommonInclude.hlsl
176 StructuredBuffer<Light> Lights : register( t8 );
2.3 Pixel Shader Continued
相比于vertex shader纲辽,前向渲染的Pixel shader相對(duì)會(huì)更加復(fù)雜一點(diǎn)颜武,這里會(huì)詳細(xì)解釋該pixel shader贫母,因?yàn)樗潜疚闹兴袖秩舅惴ǖ幕A(chǔ)。
2.3.1 材質(zhì)
首先盒刚,我們需要收到材質(zhì)的所有材質(zhì)屬性腺劣,如果一個(gè)材質(zhì)包含紋理和相應(yīng)的組件(component),這些紋理會(huì)在光照計(jì)算之前被采樣因块。在所有的材質(zhì)屬性被初始化后橘原,場(chǎng)景中所有的燈光會(huì)被遍歷,光照貢獻(xiàn)會(huì)隨著材質(zhì)屬性的積累和調(diào)整而產(chǎn)生最終的像素顏色涡上。
ForwardRendering.hlsl
19 [earlydepthstencil]
20 float4 PS_main( VertexShaderOutput IN ) : SV_TARGET
21 {
22 // Everything is in view space.
23 float4 eyePos = { 0, 0, 0, 1 };
24 Material mat = Mat;
函數(shù)之前的[earlydepthstencil]屬性表明GPU應(yīng)該先做早期深度和模板剔除( early depth and stencil culling)[10]趾断,這會(huì)讓depth/stencil測(cè)試在pixel shader之前執(zhí)行。這個(gè)屬性不能用于使用SV_Depth語(yǔ)義來(lái)修改深度的shader吩愧。因?yàn)檫@個(gè)pixel shader只使用了SV_TARGET語(yǔ)義來(lái)輸出顏色芋酌,因此當(dāng)一個(gè)像素被reject時(shí)可以利用早期深度和模板測(cè)試(early depth/stencil test)來(lái)提升性能。大部分的GPU都會(huì)執(zhí)行early depth/stencil test雁佳,甚至在沒(méi)有[earlydepthstencil]屬性的情況下脐帝,雖然添加這個(gè)屬性不會(huì)有一個(gè)明顯的性能影響,但我還是保留這個(gè)屬性糖权。
因?yàn)樗械墓庹沼?jì)算都在視圖空間堵腹,所以眼睛的位置(相機(jī)的位置)總是(0, 0, 0),這是使用視圖空間積極的一面星澳,因此相機(jī)的位置不需要另一個(gè)參數(shù)傳遞給shader疚顷。
第24行拷貝了一個(gè)材質(zhì),這是因?yàn)槿绻嘘P(guān)聯(lián)的紋理到材質(zhì)屬性禁偎,材質(zhì)的屬性在shader中將會(huì)發(fā)生改變(會(huì)從紋理中加載相應(yīng)的屬性)腿堤。因?yàn)椴馁|(zhì)屬性存儲(chǔ)在一個(gè)常量緩沖區(qū),沒(méi)有辦法直接更新一個(gè)常量緩沖區(qū)中的uniform變量如暖,所以使用了一個(gè)臨時(shí)變量笆檀。
2.3.1.1 漫反射(Diffuse)
Diffuse顏色是讀取到的第一個(gè)材質(zhì)屬性。
// ForwardRendering.hlsl
26 float4 diffuse = mat.DiffuseColor;
27 if ( mat.HasDiffuseTexture )
28 {
29 float4 diffuseTex = DiffuseTexture.Sample( LinearRepeatSampler, IN.texCoord );
30 if ( any( diffuse.rgb ) )
31 {
32 diffuse *= diffuseTex;
33 }
34 else
35 {
36 diffuse = diffuseTex;
37 }
38 }
默認(rèn)的diffuse顏色是材質(zhì)中的DiffuseColor装处,如果該材質(zhì)有一個(gè)關(guān)聯(lián)的diffuse紋理误债,該顏色會(huì)與diffuse紋理中加載的顏色進(jìn)行混合浸船。如果材質(zhì)中的顏色是黑色(0, 0, 0)妄迁,會(huì)直接使用diffuse紋理加載的顏色。HLSL內(nèi)置的any函數(shù)可以用來(lái)判斷是否有一個(gè)顏色通道不為0李命。
2.3.1.2 不透明度(Opacity)
決定了像素的alpha值登淘。
ForwardRendering.hlsl
41 float alpha = diffuse.a;
42 if ( mat.HasOpacityTexture )
43 {
44 // If the material has an opacity texture, use that to override the diffuse alpha.
45 alpha = OpacityTexture.Sample( LinearRepeatSampler, IN.texCoord ).r;
46 }
默認(rèn)情況下,片元(fragment)的透明值(也即alpha值)由diffuse顏色的alpha決定封字。如果材質(zhì)有關(guān)聯(lián)的opacity紋理黔州,opacity紋理的紅色通道(r通道)會(huì)代替diffuse紋理中的alpha值耍鬓,來(lái)作為diffuse顏色的alpha值。大多數(shù)情況下流妻,opacity紋理只存儲(chǔ)一個(gè)通道在顏色的第一個(gè)component牲蜀,被采樣時(shí)也會(huì)返回到第一個(gè)component。為了從單通道紋理中讀取值绅这,我們必須從紅色(r)通道中讀涣达,而不是alpha通道,因?yàn)閱瓮ǖ兰y理中的alpha值始終為1.
2.3.1.3 環(huán)境光和自發(fā)光(Ambient和Emissive)
環(huán)境光(Ambient)和自發(fā)光(Emissive)顏色的讀取與diffuse顏色類似证薇,環(huán)境光顏色也需要與材質(zhì)中的GlobalAmbient變量進(jìn)行混合度苔。
// ForwardRendering.hlsl
48 float4 ambient = mat.AmbientColor;
49 if ( mat.HasAmbientTexture )
50 {
51 float4 ambientTex = AmbientTexture.Sample( LinearRepeatSampler, IN.texCoord );
52 if ( any( ambient.rgb ) )
53 {
54 ambient *= ambientTex;
55 }
56 else
57 {
58 ambient = ambientTex;
59 }
60 }
61 // Combine the global ambient term.
62 ambient *= mat.GlobalAmbient;
63
64 float4 emissive = mat.EmissiveColor;
65 if ( mat.HasEmissiveTexture )
66 {
67 float4 emissiveTex = EmissiveTexture.Sample( LinearRepeatSampler, IN.texCoord );
68 if ( any( emissive.rgb ) )
69 {
70 emissive *= emissiveTex;
71 }
72 else
73 {
74 emissive = emissiveTex;
75 }
76 }
2.3.1.4 Specular Power
接下來(lái)會(huì)計(jì)算高光強(qiáng)度。
// ForwardRendering.hlsl
78 if ( mat.HasSpecularPowerTexture )
79 {
80 mat.SpecularPower = SpecularPowerTexture.Sample( LinearRepeatSampler, IN.texCoord ).r \
81 * mat.SpecularScale;
82 }
如果材質(zhì)有關(guān)聯(lián)的SpecularPower紋理浑度,該紋理的紅色component會(huì)被采樣寇窑,然后使用縮放材質(zhì)中的SpecularScale對(duì)其進(jìn)行縮放。在本例中箩张,材質(zhì)中的SpecularPower會(huì)被紋理中縮放過(guò)的值所取代甩骏。
2.3.1.5 法線(Normal)
If the material has either an associated normal map or a bump map, normal mapping or bump mapping will be performed to compute the normal vector. If neither a normal map nor a bump map texture is associated with the material, the input normal is used as-is.
如果紋理中有關(guān)聯(lián)的法線貼圖(normal map)或凹凸貼圖(bump map),會(huì)執(zhí)行法線映射或凹凸映射來(lái)計(jì)算法線向量先慷,如果兩者都沒(méi)有横漏,則使用輸入的法線(從vertex shader中輸出)。
// ForwardRendering.hlsl
85 // Normal mapping
86 if ( mat.HasNormalTexture )
87 {
88 // For scenes with normal mapping, I don't have to invert the binormal.
89 float3x3 TBN = float3x3( normalize( IN.tangentVS ),
90 normalize( IN.binormalVS ),
91 normalize( IN.normalVS ) );
92
93 N = DoNormalMapping( TBN, NormalTexture, LinearRepeatSampler, IN.texCoord );
94 }
95 // Bump mapping
96 else if ( mat.HasBumpTexture )
97 {
98 // For most scenes using bump mapping, I have to invert the binormal.
99 float3x3 TBN = float3x3( normalize( IN.tangentVS ),
100 normalize( -IN.binormalVS ),
101 normalize( IN.normalVS ) );
102
103 N = DoBumpMapping( TBN, BumpTexture, LinearRepeatSampler, IN.texCoord, mat.BumpIntensity );
104 }
105 // Just use the normal from the model.
106 else
107 {
108 N = normalize( float4( IN.normalVS, 0 ) );
109 }
2.3.1.6 法線映射(Normal Mapping)
函數(shù)DoNormalMapping會(huì)使用TBN(切線(tangent)熟掂,副切線/副法線(bitangent/binormal)缎浇,法線(normal))矩陣和法線貼圖計(jì)算法線映射(Normal Mapping)。
- 一個(gè)獅子頭的法線貼圖示例. [11]
CommonInclude.hlsl
323 float3 ExpandNormal( float3 n )
324 {
325 return n * 2.0f - 1.0f;
326 }
327
328 float4 DoNormalMapping( float3x3 TBN, Texture2D tex, sampler s, float2 uv )
329 {
330 float3 normal = tex.Sample( s, uv ).xyz;
331 normal = ExpandNormal( normal );
332
333 // Transform normal from tangent space to view space.
334 normal = mul( normal, TBN );
335 return normalize( float4( normal, 0 ) );
336 }
法線映射很簡(jiǎn)單赴肚,這文章法線映射中有詳細(xì)的解釋素跺。簡(jiǎn)單來(lái)說(shuō)我們只需要從法線貼圖中采樣法線,展開(kāi)法線到[-1..1]范圍誉券,然后通過(guò)后乘TBN矩陣將其從切線空間變換到視圖空間指厌。
2.3.1.7 凹凸映射(Bump Mapping)
凹凸映射原理類似,除了bump紋理中不是直接存儲(chǔ)的法線踊跟,而是[0..1]范圍的高度值踩验。法線可以通過(guò)計(jì)算bump紋理在U和V坐標(biāo)方向上高度的梯度(gradient)來(lái)生成,通過(guò)兩個(gè)方向上梯度的叉積(cross product)來(lái)得到紋理空間的法線商玫,然后通過(guò)后乘TBN矩陣將其從切線空間變換到視圖空間箕憾。可以通過(guò)縮放從bump貼圖中讀取的高度值來(lái)產(chǎn)生更大(更小)的凹凸拳昌。
- 凹凸紋理(左)和相應(yīng)的人頭模型(右)[12]
CommonInclude.hlsl
333 float4 DoBumpMapping( float3x3 TBN, Texture2D tex, sampler s, float2 uv, float bumpScale )
334 {
335 // Sample the heightmap at the current texture coordinate.
336 float height = tex.Sample( s, uv ).r * bumpScale;
337 // Sample the heightmap in the U texture coordinate direction.
338 float heightU = tex.Sample( s, uv, int2( 1, 0 ) ).r * bumpScale;
339 // Sample the heightmap in the V texture coordinate direction.
340 float heightV = tex.Sample( s, uv, int2( 0, 1 ) ).r * bumpScale;
341
342 float3 p = { 0, 0, height };
343 float3 pU = { 1, 0, heightU };
344 float3 pV = { 0, 1, heightV };
345
346 // normal = tangent x bitangent
347 float3 normal = cross( normalize(pU - p), normalize(pV - p) );
348
349 // Transform normal from tangent space to view space.
350 normal = mul( normal, TBN );
351
352 return float4( normal, 0 );
353 }
這里并不能保證bump映射算法100%正確袭异,沒(méi)有找到相關(guān)資源說(shuō)如何正確進(jìn)行bump映射,如果有更好的方來(lái)執(zhí)行bump映射炬藤,請(qǐng)留言討論御铃。
如果材質(zhì)沒(méi)有關(guān)聯(lián)的法線貼圖或凹凸貼圖碴里,直接使用vertex shader中輸出的法線向量。
現(xiàn)在我們用了計(jì)算光照所需要的所有數(shù)據(jù)上真。
2.3.2 光照(Lighting)
The lighting calculations for the forward rendering technique are performed in the DoLighting function. This function accepts the following arguments:
前向渲染技術(shù)的光照計(jì)算在函數(shù)DoLighting執(zhí)行咬腋,該函數(shù)接受如下的參數(shù):
- lights: 光源的數(shù)組(structured buffer)。
- mat: 我們前面計(jì)算的材質(zhì)屬性睡互。
- eyePos: 視圖空間的相機(jī)坐標(biāo)(總是(0, 0, 0))帝火。
- P: 被著色點(diǎn)在視圖空間中的位置。
- N: 被著色點(diǎn)在視圖空間中的法線湃缎。
函數(shù)DoLighting返回一個(gè)包含場(chǎng)景中所有燈光的diffuse和高光光照貢獻(xiàn)的DoLighting結(jié)構(gòu)犀填。
// ForwardRendering.hlsl
425 // This lighting result is returned by the
426 // lighting functions for each light type.
427 struct LightingResult
428 {
429 float4 Diffuse;
430 float4 Specular;
431 };
432
433 LightingResult DoLighting( StructuredBuffer<Light> lights, Material mat, float4 eyePos, float4 P, float4 N )
434 {
435 float4 V = normalize( eyePos - P );
436
437 LightingResult totalResult = (LightingResult)0;
438
439 for ( int i = 0; i < NUM_LIGHTS; ++I )
440 {
441 LightingResult result = (LightingResult)0;
442
443 // Skip lights that are not enabled.
444 if ( !lights[i].Enabled ) continue;
445 // Skip point and spot lights that are out of range of the point being shaded.
446 if ( lights[i].Type != DIRECTIONAL_LIGHT &&
447 length( lights[i].PositionVS - P ) > lights[i].Range ) continue;
448
449 switch ( lights[i].Type )
450 {
451 case DIRECTIONAL_LIGHT:
452 {
453 result = DoDirectionalLight( lights[i], mat, V, P, N );
454 }
455 break;
456 case POINT_LIGHT:
457 {
458 result = DoPointLight( lights[i], mat, V, P, N );
459 }
460 break;
461 case SPOT_LIGHT:
462 {
463 result = DoSpotLight( lights[i], mat, V, P, N );
464 }
465 break;
466 }
467 totalResult.Diffuse += result.Diffuse;
468 totalResult.Specular += result.Specular;
469 }
470
471 return totalResult;
472 }
視線向量(V)通過(guò)眼睛位置和被著色像素點(diǎn)在視圖空間的位置計(jì)算而來(lái)。
燈光緩沖區(qū)的迭代在439行嗓违,因?yàn)楸唤玫墓庠春统龇秶墓庠床粫?huì)貢獻(xiàn)任何光照九巡,所以可以跳過(guò)這些光源,否則會(huì)根據(jù)光源類型來(lái)調(diào)用相應(yīng)的光照函數(shù)蹂季。
每個(gè)不同類型的光源會(huì)計(jì)算他們的diffuse和specular光照貢獻(xiàn)冕广,因?yàn)閷?duì)不同類型光源,計(jì)算diffuse和specular的方式相同偿洁,所以我會(huì)定義不依賴于光源類型的函數(shù)來(lái)計(jì)算diffuse和specular光照貢獻(xiàn)撒汉。
2.3.2.1 漫反射光照(Diffuse Lighting)
函數(shù)DoDiffuse非常簡(jiǎn)單,并且只需要知道光向量(L)和表面法線(N)涕滋。
- 漫反射光照
// CommonInclude.hlsl
355 float4 DoDiffuse( Light light, float4 L, float4 N )
356 {
357 float NdotL = max( dot( N, L ), 0 );
358 return light.Color * NdotL;
359 }
漫反射光照的計(jì)算采用光向量(L)和表面法線(N)的點(diǎn)積(dot product)睬辐,兩個(gè)向量需要是歸一化的(normalized),通過(guò)將點(diǎn)積的結(jié)果與燈光的顏色相乘來(lái)得到該燈光的光照貢獻(xiàn)宾肺。
下面溯饵,我們來(lái)計(jì)算燈光的specular貢獻(xiàn)。
2.3.2.2 高光光照(Specular Lighting)
函數(shù)DoSpecular用來(lái)計(jì)算燈光的specular貢獻(xiàn)锨用,除了光向量(L)和表面法線(N)丰刊,該函數(shù)也需要視線向量(V來(lái)計(jì)算該燈光的specular貢獻(xiàn)。
- Specular Lighting
// CommonInclude.hlsl
361 float4 DoSpecular( Light light, Material material, float4 V, float4 L, float4 N )
362 {
363 float4 R = normalize( reflect( -L, N ) );
364 float RdotV = max( dot( R, V ), 0 );
365
366 return light.Color * pow( RdotV, material.SpecularPower );
367 }
因?yàn)楣饩€向量L是從被著色點(diǎn)到光源的向量增拥,所以在計(jì)算反射向量(R)之前需要將L取負(fù)啄巧,以使向量從光源指向被著色點(diǎn)。反射向量(R)和視線向量(V)的點(diǎn)積被用來(lái)計(jì)算的高光強(qiáng)度值的冪掌栅,然后使用光線顏色進(jìn)行調(diào)制秩仆,切記范圍是(0..1)高光強(qiáng)度的是無(wú)意義的。
2.3.2.3 衰減(Attenuation)
衰減(Attenuation)是光的強(qiáng)度下降渣玲,因?yàn)楣怆x被著色的點(diǎn)更遠(yuǎn)逗概。在傳統(tǒng)的光照模型中,衰減被計(jì)算為三個(gè)衰減因子的和乘以到光源的距離的倒數(shù)(如衰減中所解釋的):
- 常量衰減
- 線性衰減
- 二次方衰減
然而忘衍,這個(gè)方法計(jì)算的衰減是假設(shè)光永遠(yuǎn)不會(huì)衰減到0的(光具有無(wú)限的范圍)逾苫。對(duì)于延遲渲染和forward+,我們必須得能表示場(chǎng)景中的燈光具有有限的范圍枚钓,所以我們以一種差分的方法來(lái)計(jì)算光的衰減铅搓。
一種可行的方法的是做一個(gè)0到1的線性插值來(lái)計(jì)算光的衰減,其中1表示靠近光源搀捷,0表示點(diǎn)到光源的距離超過(guò)光的范圍星掰,然而線性衰減看起來(lái)不是很真實(shí),事實(shí)上衰減更像是二次方函數(shù)的倒數(shù)嫩舟。
我打算使用HLSL內(nèi)置的smoothstep函數(shù)氢烘,該函數(shù)返回一個(gè)在最小和最大值之間平滑的插值。
- 圖 HLSL內(nèi)置的smoothstep函數(shù)
// CommonInclude.hlsl
396 // Compute the attenuation based on the range of the light.
397 float DoAttenuation( Light light, float d )
398 {
399 return 1.0f - smoothstep( light.Range * 0.75f, light.Range, d );
400 }
如果到光源的距離(d)/小于光范圍的?家厌,函數(shù)smoothstep返回0播玖,如果距離大于光的范圍則返回1,通過(guò)從1中減去這個(gè)值就可以得到我們需要的衰減饭于。
或者蜀踏,我們可以通過(guò)在上面的方程中參數(shù)化0.75f來(lái)調(diào)整光的衰減的平滑度。平滑系數(shù)0.0應(yīng)該導(dǎo)致光的強(qiáng)度保持1.0掰吕,直到光的最大范圍果覆,而平滑系數(shù)1.0應(yīng)該導(dǎo)致光的強(qiáng)度內(nèi)插通過(guò)整個(gè)光的范圍。
- 圖 可變的衰減平滑
現(xiàn)在殖熟,讓我們將diffuse局待,specular和衰減因子組合在一起為不同的燈光類型計(jì)算光光照貢獻(xiàn)。
2.3.2.4 點(diǎn)光源(Point Light)
點(diǎn)光源組合衰減菱属,diffuse和specular來(lái)決定最終的光照貢獻(xiàn)燎猛。
// ForwardRendering.hlsl
390 LightingResult DoPointLight( Light light, Material mat, float4 V, float4 P, float4 N )
391 {
392 LightingResult result;
393
394 float4 L = light.PositionVS - P;
395 float distance = length( L );
396 L = L / distance;
397
398 float attenuation = DoAttenuation( light, distance );
399
400 result.Diffuse = DoDiffuse( light, L, N ) *
401 attenuation * light.Intensity;
402 result.Specular = DoSpecular( light, mat, V, L, N ) *
403 attenuation * light.Intensity;
404
405 return result;
406 }
在400和401行,diffuse和specular的貢獻(xiàn)被衰減和光強(qiáng)度(Intensity)進(jìn)行縮放照皆。
2.3.2.5 聚光燈(Spot Light)
除了衰減因子重绷,聚光燈還有一個(gè)錐角。在這種情況下膜毁,光的強(qiáng)度是由光向量(L)和聚光燈方向之間的點(diǎn)積決定的昭卓。如果光向量與聚光方向之間的夾角小于聚光錐角,則點(diǎn)應(yīng)由聚光燈點(diǎn)亮瘟滨。否則聚光燈不應(yīng)該為被著色點(diǎn)的點(diǎn)提供任何光照候醒。DoSpotCone函數(shù)將根據(jù)聚光錐的角度計(jì)算光強(qiáng)。
// CommonInclude.hlsl
375 float DoSpotCone( Light light, float4 L )
376 {
377 // If the cosine angle of the light's direction
378 // vector and the vector from the light source to the point being
379 // shaded is less than minCos, then the spotlight contribution will be 0.
380 float minCos = cos( radians( light.SpotlightAngle ) );
381 // If the cosine angle of the light's direction vector
382 // and the vector from the light source to the point being shaded
383 // is greater than maxCos, then the spotlight contribution will be 1.
384 float maxCos = lerp( minCos, 1, 0.5f );
385 float cosAngle = dot( light.DirectionVS, -L );
386 // Blend between the minimum and maximum cosine angles.
387 return smoothstep( minCos, maxCos, cosAngle );
388 }
首先杂瘸,計(jì)算聚光燈錐的余弦倒淫,如果聚光燈的方向和光向量(L)之間的點(diǎn)積小于最小該余弦值,那么光的貢獻(xiàn)將是0败玉。如果點(diǎn)積大于最大余弦角敌土,那么聚光燈的貢獻(xiàn)將是1镜硕。
- 圖 聚光燈的最小和最大余弦角
最大余弦角比最小余弦角小,這似乎是違反直覺(jué)的返干,但是不要忘記0°的余弦是1,90°的余弦是0兴枯。
DoSpotLight函數(shù)將計(jì)算聚光燈的貢獻(xiàn),與計(jì)算點(diǎn)光源的貢獻(xiàn)類似矩欠,另外算上聚光燈的余弦角财剖。
// ForwardRendering.hlsl
418 LightingResult DoSpotLight( Light light, Material mat, float4 V, float4 P, float4 N )
419 {
420 LightingResult result;
421
422 float4 L = light.PositionVS - P;
423 float distance = length( L );
424 L = L / distance;
425
426 float attenuation = DoAttenuation( light, distance );
427 float spotIntensity = DoSpotCone( light, L );
428
429 result.Diffuse = DoDiffuse( light, L, N ) *
430 attenuation * spotIntensity * light.Intensity;
431 result.Specular = DoSpecular( light, mat, V, L, N ) *
432 attenuation * spotIntensity * light.Intensity;
433
434 return result;
435 }
2.3.2.6 方向光(Directional Lights)
方向光是最簡(jiǎn)單的燈光類型,因?yàn)樗鼈冊(cè)诒恢c(diǎn)上不會(huì)衰減癌淮。
// ForwardRendering.hlsl
406 LightingResult DoDirectionalLight( Light light, Material mat, float4 V, float4 P, float4 N )
407 {
408 LightingResult result;
409
410 float4 L = normalize( -light.DirectionVS );
411
412 result.Diffuse = DoDiffuse( light, L, N ) * light.Intensity;
413 result.Specular = DoSpecular( light, mat, V, L, N ) * light.Intensity;
414
415 return result;
416 }
2.3.2.7 最終著色
現(xiàn)在我們有了材質(zhì)屬性和場(chǎng)景中所有燈光的疊加照明效果躺坟,我們可以將它們結(jié)合起來(lái)進(jìn)行最終的著色。
// ForwardRendering.hlsl
111 float4 P = float4( IN.positionVS, 1 );
112
113 LightingResult lit = DoLighting( Lights, mat, eyePos, P, N );
114
115 diffuse *= float4( lit.Diffuse.rgb, 1.0f ); // Discard the alpha value from the lighting calculations.
116
117 float4 specular = 0;
118 if ( mat.SpecularPower > 1.0f ) // If specular power is too low, don't use it.
119 {
120 specular = mat.SpecularColor;
121 if ( mat.HasSpecularTexture )
122 {
123 float4 specularTex = SpecularTexture.Sample( LinearRepeatSampler, IN.texCoord );
124 if ( any( specular.rgb ) )
125 {
126 specular *= specularTex;
127 }
128 else
129 {
130 specular = specularTex;
131 }
132 }
133 specular *= lit.Specular;
134 }
135
136 return float4( ( ambient + emissive + diffuse + specular ).rgb,
137 alpha * mat.Opacity );
138
139 }
在第113行乳蓄,光照貢獻(xiàn)是使用剛才描述的DoLighting函數(shù)計(jì)算的咪橙。
在第115行,材質(zhì)的漫反射顏色(diffuse color)是由光的diffuse貢獻(xiàn)調(diào)節(jié)的栓袖。
如果材質(zhì)的高光強(qiáng)度低于1.0匣摘,則不會(huì)考慮它參與最終著色。如果材質(zhì)沒(méi)有高光裹刮,一些美術(shù)師會(huì)指定一個(gè)小于1的高光強(qiáng)度音榜。在這種情況下,我們只是忽略了高光的貢獻(xiàn)和材質(zhì)被認(rèn)為是只有漫反射的(lambert反射)捧弃。否則赠叼,如果材質(zhì)有與之相關(guān)的高光紋理,它將被采樣违霞,并與材質(zhì)的高光顏色相結(jié)合嘴办,然后再用光的高光貢獻(xiàn)進(jìn)行調(diào)制。
最后的像素顏色是環(huán)境买鸽、自發(fā)光涧郊、漫反射和高光顏色的總和,像素的不透明度由pixel shader中先前確定的alpha值決定眼五。