【GPU精粹與Shader程式設計】(八) 《GPU Pro 1》全書核心內容提煉總結
本文由@淺墨_毛星雲 出品,首發於知乎專欄,轉載請註明出處
本文是【GPU精粹與Shader程式設計】系列的第八篇文章,全文共兩萬餘字。文章盤點、提煉和總結了《GPU Pro 1》全書總計22章的核心內容。
題圖來自《荒野大鏢客2》。
全文內容目錄
本文將對《GPU Pro 1》全書中游戲開發與渲染相關,相對更具含金量的5個部分,共22章的內容進行提煉與總結,詳細列舉如下:
- Part I. 遊戲渲染技術剖析 Game Postmortems
- 一、《孢子(Spore)》中的風格化渲染 | Stylized Rendering in Spore
- 二、《狂野西部:生死同盟》中的渲染技術 | Rendering Techniques in Call of Juarez: Bound in Blood
- 三、《正當防衛2》中的大世界製作經驗與教訓 | Making it Large, Beautiful, Fast, and Consistent: Lessons Learned
- 四、《礦工戰爭》中的可破壞體積地形 | Destructible Volumetric Terrain
- Part II. 渲染技術 Rendering Techniques
- 五、基於高度混合的四叉樹位移貼圖 | Quadtree Displacement Mapping with Height Blending
- 六、使用幾何著色器的NPR效果 | NPR Effects Using the Geometry Shader
- 七、後處理Alpha混合 | Alpha Blending as a Post-Process
- 八、虛擬紋理對映簡介 | Virtual Texture Mapping 101
- Part III. 全域性光照 Global Illumination
- 九、基於間接光照快速,基於模板的多解析度潑濺 Fast, Stencil-Based Multiresolution Splatting for Indirect Illumination
- 十、螢幕空間定向環境光遮蔽 Screen-Space Directional Occlusion (SSDO)
- 十一、基於幾何替代物技術的實時多級光線追蹤 | Real-Time Multi-Bounce Ray-Tracing with Geometry Impostors
- Part IV. 影象空間 Image Space
- 十二、GPU上各項異性的Kuwahara濾波 | Anisotropic Kuwahara Filtering on the GPU
- 十三、基於後處理的邊緣抗鋸齒 | Edge Anti-aliasing by Post-Processing
- 十四、基於Floyd-Steinberg 半色調的環境對映 | Environment Mapping with Floyd-Steinberg Halftoning
- 十五、用於粒狀遮擋剔除的分層項緩衝 | Hierarchical Item Buffers for Granular Occlusion Culling
- 十六、後期製作中的真實景深 | Realistic Depth of Field in Postproduction
- 十七、實時螢幕空間的雲層光照 | Real-Time Screen Space Cloud Lighting
- 十八、螢幕空間次表面散射 | Screen-Space Subsurface Scattering
- Part V. 陰影 Shadows
- 十九、快速傳統陰影濾波 | Fast Conventional Shadow Filtering
- 二十、混合最小/最大基於平面的陰影貼圖 | Hybrid Min/Max Plane-Based Shadow Maps
- 二十一、基於四面體對映實現全向光陰影對映 | Shadow Mapping for Omnidirectional Light Using Tetrahedron Mapping
- 二十二、螢幕空間軟陰影 | Screen Space Soft Shadows
《GPU Pro 1》其書
《GPU Pro 1》全稱為《GPU Pro : Advanced Rendering Techniques》,其作為GPU Pro系列的開山之作,出版於2010年,匯聚了當代業界前沿的圖形學技術。全書共10個部分,41章。
一個有趣的細節是,《GPU Pro 1》是GPU Pro系列7本書中,頁數最多的一本,共712頁。
圖 《GPU Pro 1》封面
《GPU Pro 1》書本配套原始碼
類似之前的GPU Gems系列的原始碼收藏GitHub repo(https://github.com/QianMo/GPU-Gems-Book-Source-Code),我也維護了的一個名為“GPU-Pro-Books-Source-Code”的GitHub倉庫,以備份GPU Pro系列珍貴的資源,也方便直接在GitHub Web端檢視業界大牛們寫的程式碼,連結如下:
Part I. 遊戲渲染技術剖析 Game Postmortems
一、《孢子(Spore)》中的風格化渲染 | Stylized Rendering in Spore
《孢子(Spore)》是一款非常有創意的遊戲。在遊戲《孢子(Spore)》中,使用了可程式設計過濾鏈系統(scriptable filter chain system)在執行時對幀進行處理,以實現遊戲整體獨特的風格化渲染。(注,在本文中,filter按語境,譯為濾波或者過濾)。
圖 《孢子》封面圖
圖 《孢子》中的風格化渲染
過濾器鏈(filter chain)可以看作一系列按順序應用的引數化的影象處理(image processing)著色器,即後處理鏈。《孢子》中的每一幀都使用此係統進行處理和合成。 除了《孢子》標準的藝術導向的視覺風格外,開發人員還建立了一組特有的濾波器,為遊戲產生截然不同的視覺風格。而在這章中,作者講到了一些在開發《孢子》時生成的視覺樣式,並分享了關於《孢子》過濾器鏈系統的設計和實現的細節。
諸如模糊(blur),邊緣檢測(edge detection)等影象處理技術的GPU實現在影象處理領域較為常見。《孢子》的開發目標是構建一個具有此類過濾器的調色系統,美術師可以利用這些過濾器來創作不同的視覺樣式。 下圖顯示了該系統在渲染管線中如何進行放置。
圖 過濾器鏈系統總覽
圖 《孢子》中以油畫方式進行渲染的飛機
下圖顯示了《孢子》中細胞階段的過濾器鏈如何使用由渲染管線的其他階段生成的多個輸入紋理,並形成最終的合成幀。
圖 《孢子》中細胞階段遊戲流體環境的複雜過濾器鏈
1.1 後處理過濾鏈系統的實現要點
過濾鏈系統實現的方面,分為兩個要點:
-
動態引數(Dynamic parameters)
-
自定義過濾器(Custom filters)
1.1.1 動態引數(Dynamic parameters)
《孢子》中的動態環境需要呼叫按幀變化引數。所以,遊戲中添加了可以通過任何過濾器訪問的每幀更新的全域性引數。例如,使用相機高度和當日時間作為行星大氣過濾器的變化引數,如下圖。
而在其他情況下,遊戲需要在給定過濾器的兩組不同引數值之間平滑插值。例如,每當天氣系統開始下雨時,全域性著色過濾器的顏色就會轉換為陰天的灰色。在系統中也添加了支援遊戲控制插值的引數,也添加了可以平滑改變濾波器強度的衰減器(fader)。
圖 按當日時間驅動的顏色過濾器。這種經過彩色壓縮的輸出會進行模糊並以bloom的方式新增到場景中
1.1.2 自定義過濾器(Custom filters)
過濾鏈系統的一個重要補充是自定義過濾器,可以將其著色器指定為引數。這意味著程式設計師可以通過向現有構建新增新著色器來輕鬆新增新的影象技術。此外,程式設計師可以通過將多個過濾器摺疊到一個實現相同視覺效果的自定義過濾器中來優化藝術家生成的過濾器鏈。
1.2 五種屏幕後處理Shader的實現思路
接著,介紹五種《孢子》中比較有意思的後處理效果。
1.2.1 油畫後處理效果 Oil Paint Filter
對於油畫過濾器(Oil Paint Filter),首先渲染畫筆描邊的法線貼圖,用於對傳入的場景進行扭曲。 然後使用相同的法線貼圖結合三個光源照亮影象空間中的筆觸(Brush stroke)。 而筆觸可以通過帶狀的粒子特效驅動,使過濾效果變得動態,並且在時間上更加連貫。
圖 《孢子》中的油畫後處理效果
用於油畫效果的畫素著色器核心程式碼如下:
# Oil Paint Effect
# kDistortionScale 0.01, kBrighten 2.0
# kNormalScales (1.5, 2.5, 1.6)
# Get the normal map and bring normals into [-1,1] range
half4 pNormalMap = tex2D ( normalMap , fragIn .uv0 );
half3 nMapNormal = 2 * pNormalMap .rgb - half3( 1, 1, 1 );
# Distort the UVs using normals ( Dependent Texture Read!)
half4 pIn = tex2D (sceneTex ,
saturate (uv - nMapNormal .xy * kDistortionScale) );
# Generate the image space lit scene
half3 fakeTangN = nMapNormal .rbg * kNormalScales;
fakeTangN = normalize (fakeTangN );
# Do this for 3 lights and sum , choose different directions
# and colors for the lights
half NDotL = saturate (dot (kLightDir , fakeTangN ));
half3 normalMappingComponent = NDotL * kLightColor ;
# Combine distorted scene with lit scene
OUT .color .rgb = pIn .rgb * normalMappingComponent * kBrighten ;
1.2.2 水彩畫後處理效果 Watercolor Filter
對於水彩畫過濾器(watercolor filter)。首先,使用傳入場景的簡易Sobel邊緣檢測版本與原始場景相乘。 然後使用平滑濾波器(smoothing filter)的四個pass對結果進行平滑,且該平滑濾波器從四周的taps中找到每個pass的最亮值。 接著,基於邊緣檢測的輪廓新增一些在平滑過程中丟失的精確度。 具體核心程式碼如下,而offset和scales是可調的引數,允許我們改變繪製塗抹筆觸的大小。
圖 《孢子》中的水彩後處理效果
《孢子》中的水彩後處理效果畫素著色器程式碼如下:
# Water Color Smoothing
# kScaleX = 0.5, kScaleY = 0.5
# offsetX1 = 1.5 * kScaleX offsetY1 = 1.5 * kScaleX
# offsetX2 = 0.5 * kScaleX offsetY2 = 0.5 * kScaleY
# Get the taps
tap0 = tex2D (sceneTex , uv + float2 (-offsetX1 ,-offsetY1 ));
tap1 = tex2D (sceneTex , uv + float2 (-offsetX2 ,-offsetY2 ));
tap2 = tex2D (sceneTex , uv + float2 (offsetX2 , offsetY2 ));
tap3 = tex2D (sceneTex , uv + float2 (offsetX1 , offsetY1 ));
# Find highest value for each channel from all four taps
ret0 = step(tap1 , tap0 );
ret1 = step(tap3 , tap2 );
tapwin1 = tap0* ret0 + tap1 * (1.0 - ret0);
tapwin2 = tap2* ret1 + tap3 * (1.0 - ret1);
ret = step(tapwin2 , tapwin1 );
OUT .color .rgb = tapwin1 * ret + (1.0 -ret) * tapwin2 ;
1.2.3 8位後處理效果 8-Bit Filter
圖 8-Bit Filter
要建立一個8位濾波器(8-Bit Filter),可以使用畫素著色器中的round函式,並通過點取樣繪製到遊戲解析度大小1/4的低解析度緩衝區中。 這是一個非常簡單的效果,使遊戲看起來像一箇舊式8位遊戲。
《孢子》中8-bit後處理效果的畫素著色器程式碼如下:
# 8 Bit Filter
# kNumBits : values between 8 and 20 look good
half4 source = tex2D (sourceTex , fragIn .uv0 );
OUT .color .rgb = round (source .rgb * kNumBits) / kNumBits ;
1.2.4 黑色電影后處理效果 Film Noir Filter
在建立黑色電影后處理效果時,首先將傳入的場景轉換為黑白。 然後進行縮放和偏移。新增一些噪聲,雨水顆粒效果是很好的畫龍點睛。
圖 《孢子》中黑色電影后處理效果
《孢子》中黑色電影后處理畫素著色器程式碼如下,其中,kNoiseTile可用於調整粒度,而kBias和kScale用作線性對比度拉伸的引數:
# Film Noir filter
# kNoiseTile is 4.0
# kBias is 0.15, kScale is 1.5
# kNoiseScale is 0.12
pIn = tex2D (sourceTex , uv);
pNoise = tex2D (noiseTex , uv * kNoiseTile) ;
# Standard desaturation
converter = half3 (0.23 , 0.66, 0.11);
bwColor = dot (pIn .rgb , converter );
# Scale and bias
stretched = saturate (bwColor - kBias) * kScale ;
# Add
OUT .color .rgb = stretched + pNoise * kNoiseScale ;
1.2.5 舊電影后處理效果 Old Film Filter
對於舊電影后處理效果,可以採用簡單的棕褐色著色與銳化濾波器(sharpen filter)相結合。 且可以使用粒子效果進行劃痕和漸暈的處理。
圖 舊電影后處理效果
《孢子》中舊電影后處理效果畫素著色器程式碼如下:
# Old Film Filter
# offsetX and offsetY are 2 pixels . With such wide taps , we
# get that weird sharpness that old photos have.
# kNoiseTile is 5.0, kNoiseScale is 0.18
# kSepiaRGB is (0.8, 0.5, 0.3)
# Get the scene and noise textures
float4 sourceColor = tex2D (sourceTex , uv);
float4 noiseColor = tex2D (noiseTex , uv * kNoiseTile );
# sharpen filter
tap0 = tex2D (sceneTex , uv + float2 (0, -offsetY ));
tap1 = tex2D (sceneTex , uv + float2 (0, offsetY ));
tap2 = tex2D (sceneTex , uv + float2 (-offsetX , 0));
tap3 = tex2D (sceneTex , uv + float2 (offsetX , 0));
sourceColor = 5 * sourceColor - (tap0 + tap1 + tap2 + tap3 );
# Sepia colorize
float4 converter = float4 (0.23 , 0.66, 0.11, 0);
float bwColor = dot (sourceColor , converter );
float3 sepia = kSepiaRGB * bwColor ;
# Add noise
OUT .color = sepia * kTintScale + noiseColor * kNoiseScale ;
關於《孢子》更多的風格化渲染的教程,可以在這裡找到:
二、《狂野西部:生死同盟》中的渲染技術 | Rendering Techniques in Call of Juarez: Bound in Blood
《狂野西部:生死同盟》(Call of Juarez: Bound in Blood)是由Techland公司開發,育碧發行,並於2009年夏季在PS3,Xbox360和PC上釋出的遊戲。
圖《狂野西部:生死同盟》封面
圖《GPU Pro 1》的封面,即是採用的《狂野西部:生死同盟》的圖片
圖 《狂野西部:生死同盟》遊戲截圖
《狂野西部:生死同盟》基於ChromeEngine 4,遊戲中大量用到了延遲著色(deferred shading)技術。
眾所周知,延遲著色 [Hargreaves 04]是一種在螢幕空間使用儲存了諸如漫反射顏色,法向量或深度值等畫素資訊的中間緩衝區(G-buffer)的技術。
G-buffer是一組螢幕大小的渲染目標(MRT),可以使用現代圖形硬體在單個pass中生成,可以顯著降低渲染負載。然後使用G-buffer作為著色演算法的輸入(例如光照方程),而無需瀏覽原始幾何體(此階段計算所需的所有資訊,如三維世界空間中的畫素的位置,可以從G-buffer中提取)。以這種方式,演算法僅對可見畫素進行操作,這極大地降低了照明計算的複雜性。
表 《狂野西部:生死同盟》中的MRT配置
延遲著色方法的主要優點是對渲染管線的簡化,節省複雜著色器資源和計算的開銷,以及能對複雜光照(如動態光源)進行簡約而健壯的管理。
延遲著色技術在與後處理渲染效果的結合方面可以獲得不錯的化學反應。在《狂野西部:生死同盟》中,延遲渲染與諸如螢幕空間環境光遮蔽(SSAO),運動模糊(motion-blur),色調對映(tone mapping)以及用於改善最終影象質量的邊緣抗鋸齒(edge anti-aliasing)等後處理效果都可以很好的結合使用。
圖 擁有動態光源和環境光遮蔽的室內場景
這章中還展示了不少《狂野西部:生死同盟》中自然現象效果的渲染方法,如雨滴,體積地面霧,light shafts,真實感天空和雲彩,水面渲染,降雨效果,以及體積光的渲染技巧。以及色調對映相關的技術。
圖 場景色調對映,在陰影區域和光照區域之間轉換
三、《正當防衛2》中的大世界製作經驗與教訓 | Making it Large, Beautiful, Fast,and Consistent: Lessons Learned
《正當防衛2(Just Cause 2)》是Avalanche Studios為PC,Xbox 360和PLAYSTATION 3開發的沙盒遊戲。遊戲的主要風格是大世界,主要視覺特徵是具有巨大渲染範圍的巨型景觀,森林、城市、沙漠、叢林各種環境不同的氣候,以及晝夜迴圈技術。
圖 《正當防衛2》封面
對於多動態光源的渲染,《正當防衛2》沒有使用延遲渲染,而是提出了一種稱作光源索引(Light indexing)的方案,該方案可以使用前向渲染渲染大量動態光源,而無需多個pass,或增加draw calls。
3.1 光照索引 Light indexing
光照索引(Light indexing)技術的核心思路是:通過RGBA8格式128 x 128的索引紋理將光照資訊提供給著色器。
將該紋理對映到攝像機位置周圍的XZ平面中,並進行點取樣。 每個紋素都對映在一個4m x 4m的區域,並持有四個該正方形相關的光源索引。這意味著我們覆蓋了512m × 512m的區域,且動態光源處於活動狀態。
活動光源儲存在單獨的列表中,可以是著色器常量,也可以是一維紋理,具體取決於平臺。雖然使用8位通道可以索引多達256個光源,但我們將其限制為64個,以便將光源資訊擬合到著色器常量中。每個光源都有兩個恆定的暫存器,儲存位置(position),倒數平方半徑(reciprocal squared radius)和顏色(color)這三個引數。
表 光源常量
此外,還有一個額外的“禁用(disabled)”光源槽位,其所有這些都設定為零。那麼總暫存器計數會達到130。當使用一維紋理時,禁用的光源用邊框顏色(border color)編碼替代。 位置和倒數平方半徑以RGBA16F格式儲存,顏色以RGBA8格式儲存。為了保持精度,位置儲存在相對於紋理中心的區域性空間中。
光源索引紋理在CPU上由全域性光源列表生成。一開始,其位置被放置在使得紋理區域被充分利用的位置,最終以儘可能小的空間,放置在攝像機之後。
在啟用並落入索引紋理區域內的光源中,根據優先順序,螢幕上的近似大小以及其他因素來選擇最相關的光源。每個光源都插入其所覆蓋的紋素的可用通道中。如果紋理畫素被四個以上的光源覆蓋,則需要丟棄此光源。
如果在插入時紋理畫素已滿,程式將根據圖塊中的最大衰減係數檢查入射光源是否應替換任何現有的光源,以減少掉光源的視覺誤差。這些誤差可能導致圖塊邊框周圍的光照不連續性。通常這些誤差很小,但當四處移動時可能非常明顯。而為了避免這個問題,可以將索引紋理對齊到紋素大小的座標中。在實踐中,光源的丟棄非常少見,通常很難發現。
圖 軸對齊世界空間中的光照索引。 放置紋理使得儘可能多的區域在視錐體內。 圖示的4m x 4m區域由兩個由R和G通道索引的光源相交。 未使用的插槽表示禁用的光源。
3.2 陰影系統 Shadowing System
陰影方面,《正當防衛2》中採用級聯陰影對映(cascaded shadow mapping)。並對高效能PC提供軟陰影(Soft shadows)選項。雖然在任何情況下都不是物理上的準確,但演算法確實會產生真正的軟陰影,而不僅僅是在許多遊戲中使用的恆定半徑模糊陰影。
圖 《正當防衛2》中的軟陰影。注意樹底部的銳利陰影逐漸變得柔和,以及注意,樹葉投下了非常柔和的陰影。
此軟陰影演算法的步驟如下:
1、在陰影貼圖中搜索遮擋物的鄰域。
2、投射陰影的樣本計為遮擋物。
3、將遮擋物中的中心樣本的平均深度差用作第二個pass中的取樣半徑,並且在該半徑內取多個標準PCF樣本並取平均值。
4、為了隱藏有限數量的樣本失真,取樣圖案以從螢幕位置產生的偽隨機角度進行旋轉。
實現Shader程式碼如下:
// Setup rotation matrix
float3 rot0 = float3(rot.xy, shadow_coord.x);
float3 rot1 = float3(float2(-1, 1) * rot.yx, shadow_coord.y);
float z = shadow_coord.z * BlurFactor;
// Find average occluder distances .
// Only shadowing samples are taken into account .
[unroll] for (int i = 0; i<SHADOW_SAMPLES; i++)
{
coord.x = dot(rot0 , offsets[i]);
coord.y = dot(rot1 , offsets[i]);
float depth = ShadowMap.Sample(ShadowDepthFilter, coord).r;
de.x = saturate(z - depth* BlurFactor);
de.y = (de.x > 0.0);
dd += de;
}
// Compute blur radius
float radius = dd.x / dd.y + BlurBias;
rot0.xy *= radius ;
rot1.xy *= radius ;
// Sample shadow with radius
[unroll] for (int k = 0; k<SHADOW_SAMPLES; k++)
{
coord.x = dot(rot0 , offsets[k]);
coord.y = dot(rot1 , offsets[k]);
shadow += ShadowMap.SampleCmpLevelZero(
ShadowComparisonFilter, coord, shadow_coord.z).r;
}
3.3 環境光遮蔽 Ambient Occlusion
對於環境遮擋(AO),使用了三種不同的技術:
- 美術師生成的AO(artist-generated AO)
- 遮擋體(Occlusion Volumes)
- SSAO [Kajalin 09]
其中,美術師生成的環境光遮蔽用於靜態模型,由材質屬性紋理中的AO通道組成。此外,美術師有時會在關鍵點放置環境遮擋幾何。對於動態物件,使用遮擋體(OcclusionVolumes)在底層幾何體上投射遮擋陰影,主要是角色和車輛下的地面。而SSAO是PC版本的可選設定,裡面使用了一種從深度緩衝匯出切線空間的方案。
其中,SSAO從深度緩衝區匯出切線空間的實現Shader程式碼如下:
// Center sample
float center = Depth . Sample ( Filter , In. TexCoord . xy ). r;
// Horizontal and vertical neighbors
float x0 = Depth.Sample ( Filter , In. TexCoord .xy , int2 (-1 , 0)). r;
float x1 = Depth.Sample ( Filter , In. TexCoord .xy , int2 ( 1 , 0)). r;
float y0 = Depth.Sample ( Filter , In. TexCoord .xy , int2 ( 0 , 1)). r;
float y1 = Depth.Sample ( Filter , In. TexCoord .xy , int2 ( 0 , -1)). r;
// Sample another step as well for edge detection
float ex0 = Depth . Sample ( Filter , In. TexCoord , int2 (-2 , 0)). r;
float ex1 = Depth . Sample ( Filter , In. TexCoord , int2 ( 2 , 0)). r;
float ey0 = Depth . Sample ( Filter , In. TexCoord , int2 ( 0 , 2)). r;
float ey1 = Depth . Sample ( Filter , In. TexCoord , int2 ( 0 , -2)). r;
// Linear depths
float lin_depth = LinearizeDepth ( center , DepthParams . xy );
float lin_depth_x0 = LinearizeDepth (x0 , DepthParams .xy );
float lin_depth_x1 = LinearizeDepth (x1 , DepthParams . xy );
float lin_depth_y0 = LinearizeDepth (y0 , DepthParams . xy );
float lin_depth_y1 = LinearizeDepth (y1 , DepthParams . xy );
// Local position ( WorldPos - EyePosition ) float3 pos = In. Dir * lin_depth ;
float3 pos_x0 = In. DirX0 * lin_depth_x0 ;
float3 pos_x1 = In. DirX1 * lin_depth_x1 ;
float3 pos_y0 = In. DirY0 * lin_depth_y0 ;
float3 pos_y1 = In. DirY1 * lin_depth_y1 ;
// Compute depth differences in screespace X and Y float dx0 = 2.0 f * x0 - center - ex0 ;
float dx1 = 2.0 f * x1 - center - ex1 ;
float dy0 = 2.0 f * y0 - center - ey0 ;
float dy1 = 2.0 f * y1 - center - ey1 ;
// Select the direction that has the straightest
// slope and compute the tangent vectors float3 tanX , tanY ;
if ( abs ( dx0 ) < abs ( dx1 ))
tanX = pos - pos_x0 ;
else
tanX = pos_x1 - pos ;
if ( abs ( dy0 ) < abs ( dy1 ))
tanY = pos - pos_y0 ;
else
tanY = pos_y1 - pos ;
tanX = normalize ( tanX ); tanY = normalize ( tanY );
float3 normal = normalize ( cross ( tanX , tanY ));
3.4 其他內容
這一章的其他內容包括:
- 角色陰影(Character Shadows)
- 軟粒子(Soft Particles)
- 抖動錯誤:處理浮點精度(The Jitter Bug: Dealing with Floating-Point Precision)
- 著色器常量管理(Shader constant management)
- 伽馬校正和sRGB混合相關問題
- 雲層渲染優化(Cloud Rendering Optimization)
- 粒子修剪(Particle Trimming)
- 記憶體優化(Memory Optimizations)
由於篇幅所限,這些內容無法展開講解。感興趣的朋友,不妨可以找到原書對應部分進行閱讀。
四、《礦工戰爭》中的可破壞體積地形 | Destructible Volumetric Terrain
這篇文章中,主要講到了遊戲《礦工戰爭(Miner Wars)》中基於體素(voxel)的可破壞體積地形技術。
《礦工戰爭(Miner Wars)》遊戲的主要特徵是多維度地形的即時破壞,並且引擎依賴預先計算的資料。 每個地形變化都會實時計算,消耗盡可能少的記憶體並且沒有明顯的延遲。
圖 《礦工戰爭》遊戲截圖
在遊戲的實現中,體素是具有以米為單位的實際尺寸的三維畫素。 每個體素都儲存有關其密度的資訊 – 是否全空,是否全滿,或介於空和滿之間,而體素的材質型別用於貼圖,破壞以及遊戲邏輯中。
文中將體素貼圖(voxel map)定義為一組體素(例如,256 x 512 x 256)的集合。每個體素貼圖包含體素的資料單元(data cells),以及包含靜態頂點緩衝區和三角形索引的渲染單元(render cells)。
《礦工戰爭》的引擎不會直接渲染體素,相反,是將體素多邊形化,在渲染或檢測碰撞之前將其轉換為三角形。使用標準的行進立方體(Marching Cubes , MC)演算法 [“Marching”09]進行多邊形化。
圖 一艘採礦船用炸藥進行隧道的挖掘
圖 具有表示體素邊界的虛線的體素圖。 此圖描繪了4 x 4個體素;
圖中的小十字代表體素內的網格點; 實線代表三維模型。
Part II. 渲染技術 Rendering Techniques
五、基於高度混合的四叉樹位移貼圖 | Quadtree Displacement Mapping with Height Blending
這章中,介紹了當前表面渲染(surface rendering)技術的概述和相關比較,主要涉及在如下幾種方法:
-
Relief Mapping | 浮雕貼圖
-
Cone step mapping (CSM) | 錐步對映
-
Relaxed cone step mapping (RCSM) | 寬鬆錐步對映
-
Parallax Occlusion Mapping(POM) | 視差遮蔽貼圖
-
Quadtree Displacement Mapping(QDM)| 四叉樹位移貼圖
內容方面,文章圍繞表面渲染技術,分別介紹了光線追蹤與表面渲染、四叉樹位移對映(Quadtree Displacement Mapping)、自陰影(Self-Shadowing)、環境光遮蔽(Ambient Occlusion)、表面混合(Surface Blending)幾個部分。為了獲得最高的質量/效能/記憶體使用率,文章建議在特定情況下使用視差對映,軟陰影,環境遮擋和表面混合方法的組合。
此外,文中還提出了具有高度混合的四叉樹位移貼圖。對於使用複雜,高解析度高度場的超高質量表面,該方法明顯會更高效。此外,使用引入的四叉樹結構提出了高效的表面混合,軟陰影,環境遮擋和自動LOD方案的解決方案。在實踐中,此技術傾向於以較少的迭代和紋理樣本產生更高質量的結果。
圖 Parallax Occlusion Mapping(POM) 視差遮蔽貼圖和Quadtree Displacement Mapping(QDM)四叉樹位移貼圖和的渲染質量比較。其中,左圖為POM;右圖為QDM。深度尺寸分別為:1.0,1.5,5.0。可以發現,在深度尺寸1.5以上時,使用POM(左圖)會看到失真。
圖 表面混合質量比較。上圖:浮雕貼圖(Relief Mapping),下圖:帶高度混合的視差遮蔽貼圖(POM with height blending)
5.1 核心實現Shader程式碼
以下為視差遮蔽貼圖(Parallax Occlusion Mapping,POM)核心程式碼:
float Size = 1.0 / LinearSearchSteps;
float Depth = 1.0;
int StepIndex = 0;
float CurrD = 0.0;
float PrevD = 1.0;
float2 p1 = 0.0;
float2 p2 = 0.0;
while (StepIndex < LinearSearchSteps)
{
Depth -= Size; // move the ray
float4 TCoord = float2 (p+(v*Depth )); // new sampling pos
CurrD = tex2D (texSMP , TCoord ).a; //new height
if (CurrD > Depth ) // check for intersection
{
p1 = float2 (Depth , CurrD );
p2 = float2 (Depth + Size , PrevD ); // store last step
StepIndex = LinearSearchSteps; // break the loop
}
StepIndex ++;
PrevD = CurrD ;
}
// Linear approximation using current and last step
// instead of binary search , opposed to relief mapping .
float d2 = p2.x - p2.y;
float d1 = p1.x - p1.y;
return (p1.x * d2 - p2.x * d1) / (d2 - d1);
四叉樹位移貼圖(Quadtree Displacement Mapping ,QDM)使用mipmap結構來表示密集的四叉樹,在高度場的基準平面上方儲存最大高度。QDM會在在交叉區域使用細化搜尋,以便在需要時找到準確的解決方案。以下為四叉樹位移貼圖(QDM)搜尋的核心程式碼:
const int MaxLevel = MaxMipLvl ;
const int NodeCount = pow (2.0, MaxLevel );
const float HalfTexel = 1.0 / NodeCount / 2.0;
float d;
float3 p2 = p;
int Level = MaxLevel ;
//We calculate ray movement vector in inter -cell numbers .
int2 DirSign = sign(v.xy);
// Main loop
while (Level >= 0)
{
//We get current cell minimum plane using tex2Dlod .
d = tex2Dlod (HeightTexture , float4 (p2.xy , 0.0 , Level )). w;
//If we are not blocked by the cell we move the ray .
if (d > p2.z)
{
//We calculate predictive new ray position .
float3 tmpP2 = p + v * d;
//We compute current and predictive position .
// Calculations are performed in cell integer numbers .
int NodeCount = pow (2, (MaxLevel - Level ));
int4 NodeID = int4((p2.xy , tmpP2 .xy) * NodeCount );
//We test if both positions are still in the same cell.
//If not , we have to move the ray to nearest cell boundary .
if (NodeID .x != NodeID .z || NodeID .y != NodeID .w)
{
//We compute the distance to current cell boundary .
//We perform the calculations in continuous space .
float2 a = (p2.xy - p.xy);
float2 p3 = (NodeID .xy + DirSign) / NodeCount ;
float2 b = (p3.xy - p.xy);
//We are choosing the nearest cell
//by choosing smaller distance .
float2 dNC = abs (p2.z * b / a);
d = min (d, min (dNC .x, dNC .y));
// During cell crossing we ascend in hierarchy .
Level +=2;
// Predictive refinement
tmpP2 = p + v * d;
}
//Final ray movement
p2 = tmpP2 ;
}
// Default descent in hierarchy
// nullified by ascend in case of cell crossing
Level --;
}
return p2;
這章也引入了一種表面混合的新方法,能更自然地適合表面混合,並且保證了更快的收斂。
文中建議使用高度資訊作為額外的混合係數,從而為混合區域和更自然的外觀新增更多種類,具體實現程式碼如下:
float4 FinalH ;
float4 f1 , f2 , f3 , f4;
//Get surface sample .
f1 = tex2D(Tex0Sampler ,TEXUV .xy).rgba;
//Get height weight .
FinalH .a = 1.0 - f1.a;
f2 = tex2D(Tex1Sampler ,TEXUV .xy).rgba;
FinalH .b = 1.0 - f2.a;
f3 = tex2D(Tex2Sampler ,TEXUV .xy).rgba;
FinalH .g = 1.0 - f3.a;
f4 = tex2D(Tex3Sampler ,TEXUV .xy).rgba;
FinalH .r = 1.0 - f4.a;
// Modify height weights by blend weights .
//Per -vertex blend weights stored in IN.AlphaBlends
FinalH *= IN.AlphaBlends ;
// Normalize .
float Blend = dot (FinalH , 1.0) + epsilon ;
FinalH /= Blend ;
//Get final blend .
FinalTex = FinalH .a * f1 + FinalH .b * f2 + FinalH .g * f3 + FinalH .r * f4;
在每個交叉點搜尋(intersection search)步驟中,使用新的混合運算子重建高度場輪廓,實現程式碼如下所示:
d = tex2D (HeightTexture ,p.xy).xyzw;
b = tex2D (BlendTexture ,p.xy). xyzw;
d *= b;
d = max (d.x ,max (d.y,max (d.z,d.w)));
六、使用幾何著色器的NPR效果 | NPR Effects Using the Geometry Shader
本章的內容關於非真實感渲染(Non-photorrealistic rendering ,NPR)。在這章中,介紹了一組利用GPU幾何著色器流水線階段實現的技術。
具體來說,文章展示瞭如何利用幾何著色器來在單通道中渲染物件及其輪廓,並對鉛筆素描效果進行了模擬。
單通道方法通常使用某種預計算來將鄰接資訊儲存到頂點中[Card and Mitchell 02],或者使用幾何著色器 [Doss 08],因為可能涉及到查詢鄰接資訊。這些演算法在單個渲染過程中生成輪廓,但物件本身仍需要第一個幾何通道。
6.1 輪廓渲染(Silhouette Rendering)
輪廓渲染是大多數NPR效果的基本元素,因為它在物體形狀的理解中起著重要作用。在本節中,提出了一種在單個渲染過程中檢測,生成和紋理化模型的新方法。
輪廓渲染(Silhouette rendering)技術中, 兩大類演算法需要實時提取輪廓:
-
基於陰影體積的方法(shadow volume-based approaches)
-
非真實感渲染(non-photorealistic rendering)
而從文獻中,可以提取兩種不同的方法:
-
物件空間演算法(object-space algorithms)
-
影象空間演算法(image-space algorithms)
但是,大多數現代演算法都在影象空間(image space)或混合空間(hybrid space)中工作。本章中主要介紹基於GPU的演算法。GPU輔助演算法可以使用多個渲染通道或單個渲染通道來計算輪廓。
為了一步完成整個輪廓渲染的過程,將會使用到幾何著色器(geometry shader)。因為幾何著色階段允許三角形操作,能獲取相鄰三角形的資訊,以及為幾何體生成新的三角形。
輪廓渲染過程在流水線的不同階段執行以下步驟:
-
頂點著色器(Vertex shader)。 頂點以通常的方式轉換到相機空間。
-
幾何著色器(Geometry shader)。 在該階段中,通過使用當前三角形及其鄰接的資訊來檢測屬於輪廓的邊緣,並生成相應的幾何體。
-
畫素著色器(Pixel shader)。 對於每個柵格化片段,生成其紋理座標,並根據從紋理獲得的顏色對畫素進行著色。
圖 管線概述:頂點著色器(左)變換傳入幾何體的頂點座標;第二步(幾何著色器)為物件的輪廓生成新幾何體。最後,畫素著色器生成正確的紋理座標。
幾何著色器輪廓檢測程式碼如下:
[maxvertexcount (21)]
void main( triangleadj VERTEXin input [6],
inout TriangleStream <VERTEXout > TriStream )
{
// Calculate the triangle normal and view direction .
float3 normalTrian = getNormal ( input [0].Pos .xyz ,
input [2].Pos .xyz , input [4].Pos .xyz );
float3 viewDirect = normalize (-input [0].Pos .xyz
- input [2]. Pos .xyz - input [4].Pos .xyz );
//If the triangle is frontfacing
[branch ]if(dot (normalTrian ,viewDirect ) > 0.0f)
{
[loop]for (uint i = 0; i < 6; i+=2)
{
// Calculate the normal for this triangle .
float auxIndex = (i+2)%6;
float3 auxNormal = getNormal ( input [i].Pos .xyz ,
input[i+1].Pos .xyz , input[auxIndex ].Pos .xyz );
float3 auxDirect = normalize (- input[i].Pos .xyz
- input [i+1].Pos .xyz - input[auxIndex ].Pos .xyz );
//If the triangle is backfacing
[branch ]if(dot (auxNormal ,auxDirect) <= 0.0f)
{
// Here we have a silhouette edge.
}
}
}
}
幾何著色器輪廓生成程式碼如下:
// Transform the positions to screen space .
float4 transPos1 = mul (input [i].Pos ,projMatrix );
transPos1 = transPos1 /transPos1 .w;
float4 transPos2 = mul (input [auxIndex ].Pos ,projMatrix );
transPos2 = transPos2 /transPos2 .w;
// Calculate the edge direction in screen space .
float2 edgeDirection = normalize (transPos2 .xy - transPos1 .xy);
// Calculate the extrude vector in screen space .
float4 extrudeDirection = float4 (normalize (
float2 (-edgeDirection.y ,edgeDirection.x)) ,0.0f ,0.0f);
// Calculate the extrude vector along the vertex
// normal in screen space.
float4 normExtrude1 = mul (input [i].Pos + input [i]. Normal
,projMatrix );
normExtrude1 = normExtrude1 / normExtrude1.w;
normExtrude1 = normExtrude1 - transPos1 ;
normExtrude1 = float4 (normalize (normExtrude1.xy),0.0f ,0.0f);
float4 normExtrude2 = mul (input [auxIndex ].Pos
+ input [auxIndex ].Normal ,projMatrix );
normExtrude2 = normExtrude2 / normExtrude2.w;
normExtrude2 = normExtrude2 - transPos2 ;
normExtrude2 = float4 (normalize (normExtrude2.xy),0.0f ,0.0f);
// Scale the extrude directions with the edge size.
normExtrude1 = normExtrude1 * edgeSize ;
normExtrude2 = normExtrude2 * edgeSize ;
extrudeDirection = extrudeDirection * edgeSize ;
// Calculate the extruded vertices .
float4 normVertex1 = transPos1 + normExtrude1;
float4 extruVertex1 = transPos1 + extrudeDirection;
float4 normVertex2 = transPos2 + normExtrude2;
float4 extruVertex2 = transPos2 + extrudeDirection;
// Create the output polygons .
VERTEXout outVert ;
outVert .Pos = float4 (normVertex1 .xyz ,1.0f);
TriStream .Append (outVert );
outVert .Pos = float4 (extruVertex1.xyz ,1.0f);
TriStream .Append (outVert );
outVert .Pos = float4 (transPos1 .xyz ,1.0f);
TriStream .Append (outVert );
outVert .Pos = float4 (extruVertex2.xyz ,1.0f);
TriStream .Append (outVert );
outVert .Pos = float4 (transPos2 .xyz ,1.0f);
TriStream .Append (outVert );
outVert .Pos = float4 (normVertex2 .xyz ,1.0f);
TriStream .Append (outVert );
TriStream .RestartStrip();
在畫素著色器中輪廓紋理對映的實現程式碼:
float4 main(PIXELin inPut ):SV_Target
{
// Initial texture coordinate .
float2 coord = float2 (0.0f,inPut.UV.z);
// Vector from the projected center bounding box to
//the location .
float2 vect = inPut .UV.xy - aabbPos ;
// Calculate the polar coordinate .
float angle = atan(vect.y/vect.x);
angle = (vect.x < 0.0 f)? angle+PI:
(vect.y < 0.0f)?angle +(2* PI): angle ;
// Assign the angle plus distance to the u texture coordinate .
coord .x = ((angle /(2* PI)) + (length (vect)* lengthPer ))* scale;
//Get the texture color .
float4 col = texureDiff .Sample (samLinear ,coord );
// Alpha test.
if(col .a < 0.1 f)
discard ;
// Return color .
return col ;
}
圖 輪廓渲染演算法的執行效果圖,輪廓剪影的實時生成和紋理化。
6.2 鉛筆素描渲染(Pencil Rendering)
基於Lee等人[Lee et al. 06]鉛筆渲染思路可以概括如下。
首先,計算每個頂點處的最小曲率(curvature)。然後,三角形和其曲率值作為每個頂點的紋理座標傳入管線。 為了對三角形的內部進行著色,頂點處的曲率用於在螢幕空間中旋轉鉛筆紋理。該鉛筆紋理會在螢幕空間中進行三次旋轉,每個曲率一次,旋轉後的結果進行混合結合。不同色調的多個紋理,儲存在紋理陣列中,同時進行使用。最終,根據光照情況在其中選擇出正確的一個。
圖 管線概述:頂點著色器將頂點轉換為螢幕空間;幾何著色器將三角形的頂點曲率分配給三個頂點。最後,畫素著色器生成三個曲率的紋理座標並計算最終顏色。
可以通過以下方式使用GPU管線實現此演算法:
-
頂點著色器(Vertex shader)。 頂點轉換為螢幕座標。頂點曲率也被變換,只有x和y分量作為二維向量傳遞。
-
幾何著色器(Geometry shader)。 將曲率值作為紋理座標分配給每個頂點。
-
畫素著色器(Pixel shader)。 計算最終顏色。
幾何著色器的實現程式碼如下:
[maxvertexcount (3)]
void main( triangle VERTEXin input [3],
inout TriangleStream <VERTEXout > TriStream )
{
// Assign triangle curvatures to the three vertices .
VERTEXout outVert ;
outVert .Pos = input [0].Pos ;
outVert .norm = input [0]. norm;
outVert .curv1 = input [0]. curv;
outVert .curv2 = input [1]. curv;
outVert .curv3 = input [2]. curv;
TriStream .Append (outVert );
outVert .Pos = input [1].Pos ;
outVert .norm = input [1]. norm;
outVert .curv1 = input [0]. curv;
outVert .curv2 = input [1]. curv;
outVert .curv3 = input [2]. curv;
TriStream .Append (outVert );
outVert .Pos = input [2].Pos ;
outVert .norm = input [2]. norm;
outVert .curv1 = input [0]. curv;
outVert .curv2 = input [1]. curv;
outVert .curv3 = input [2]. curv;
TriStream .Append (outVert );
TriStream . RestartStrip();
}
畫素著色器的實現程式碼如下:
float4 main(PIXELin inPut ):SV_Target
{
float2 xdir = float2 (1.0f ,0.0f);
float2x2 rotMat ;
// Calculate the pixel coordinates .
float2 uv = float2 (inPut .Pos .x/width ,inPut .Pos .y/height );
// Calculate the rotated coordinates .
float2 uvDir = normalize (inPut .curv1 );
float angle = atan(uvDir .y/uvDir.x);
angle = (uvDir .x < 0.0 f)? angle +PI:
(uvDir .y < 0.0f)? angle +(2* PI): angle ;
float cosVal = cos (angle );
float sinVal = sin (angle );
rotMat [0][0] = cosVal ;
rotMat [1][0] = -sinVal ;
rotMat [0][1] = sinVal ;
rotMat [1][1] = cosVal ;
float2 uv1 = mul (uv ,rotMat );
uvDir = normalize (inPut.curv2 );
angle = atan(uvDir .y/uvDir.x);
angle = (uvDir .x < 0.0 f)? angle +PI:
(uvDir .y < 0.0f)? angle +(2* PI): angle ;
cosVal = cos (angle );
sinVal = sin (angle );
rotMat [0][0] = cosVal ;
rotMat [1][0] = -sinVal ;
rotMat [0][1] = sinVal ;
rotMat [1][1] = cosVal ;
float2 uv2 = mul (uv ,rotMat );
uvDir = normalize (inPut .curv3 );
angle = atan(uvDir.y/uvDir.x);
angle = (uvDir .x < 0.0 f)? angle +PI:
(uvDir .y < 0.0f)?angle +(2* PI): angle ;
cosVal = cos (angle );
sinVal = sin (angle );
rotMat [0][0] = cosVal ;
rotMat [1][0] = -sinVal ;
rotMat [0][1] = sinVal ;
rotMat [1][1] = cosVal ;
float2 uv3 = mul (uv ,rotMat );
// Calculate the light incident at this pixel.
float percen = 1.0f - max (dot (normalize (inPut .norm),
lightDir ) ,0.0);
// Combine the three colors .
float4 color = (texPencil .Sample (samLinear ,uv1 )*0.333 f)
+( texPencil .Sample (samLinear ,uv2 )*0.333 f)
+( texPencil .Sample (samLinear ,uv3 )*0.333 f);
// Calculate the final color .
percen = (percen *S) + O;
color .xyz = pow (color .xyz ,float3 (percen ,percen ,percen ));
return color;
}
最終的渲染效果:
圖 鉛筆渲染效果圖
七、後處理Alpha混合 | Alpha Blending as a Post-Process
在這篇文章中提出了一種新的Alpha混合技術,螢幕空間Alpha遮罩( Screen-Space Alpha Mask ,簡稱SSAM)。該技術首次運用於賽車遊戲《Pure》中。《Pure》發行於2008年夏天,登陸平臺為Xbox360,PS3和PC。
圖 《Pure》中的場景(tone mapping & bloom效果)
在《Pure》的開發過程中,明顯地需要大量的alpha混合(alpha blending)操作。但是眾所周知,傳統的計算機圖形學的難題之一,就是正確地進行alpha混合操作,並且往往在效能和視覺質量之間,經常很難權衡。
實際上,由於不願意承擔效能上的風險,一些遊戲會完全去避免使用alpha混合。有關alpha混合渲染所帶來的問題的全面介紹,可以參考[Thibieroz 08],以及[Porter and Duff 84]。
在這篇文章中,提出了一種新穎的(跨平臺)解決方案,用於樹葉的alpha混合,這種解決方案可以提高各種alpha測試級渲染的質量,為它們提供真正的alpha混合效果。
文中設計的解決方案——螢幕空間Alpha遮罩(Screen-Space Alpha Mask ,簡稱SSAM),是一種採用渲染技術實現的多通道方法,如下圖。無需任何深度排序或幾何分割。
在《Pure》中使用的SSAM技術對環境的整體視覺質量有著深遠的影響。 效果呈現出柔和自然的外觀,無需犧牲畫面中的任何細節。
圖 SSAM的技術思路圖示
此解決方案可以產生與alpha混合相同的結果,同時使用alpha測試技術正確解決每個畫素的內部重疊(和深度交集)。
文中使用全屏幕後處理高效地執行延遲alpha混合(deferred alpha blending),類似於將幀混合操作設定為ADD的幀緩衝混合;源和目標引數分別設定為SRCALPHA和INVSRCALPHA。
混合輸入被渲染成三個單獨的渲染目標(render targets),然後繫結到紋理取樣器(texture samplers),由最終的組合後處理畫素著色器引用。
在記憶體資源方面,至少需要三個螢幕解析度的渲染目標,其中的兩個至少具有三個顏色的通道(rtOpaque & rtFoliage),而另一個至少有兩個通道(rtMask)和一個深度緩衝區(rtDepth) 。
下面列舉一些SSAM的優點和缺點。
SSAM的優點:
-
樹葉邊緣與周圍環境平滑融合。
-
使用alpha測試技術,在每畫素的基礎上對內部重疊和相互穿透的圖元進行排序。
-
該效果使用簡單,低成本的渲染技術實現,不需要任何幾何排序或拆分(只需要原始排程順序的一致性)。
-
無論場景複雜度和overdraw如何,最終的混合操作都是以線性成本(每畫素一次)來執行運算。
-
該效果與能渲染管線中的其他alpha混合階段(如粒子等)完美整合。
-
與其他優化(如將光照移到頂點著色器)以及優化每個通道的著色器等方法結合使用時,總體效能可能會高於基於MSAA(MultiSampling Anti-Aliasing,多重取樣抗鋸齒)的技術。
SSAM的缺點:
-
需要額外的渲染Pass的開銷。
-
記憶體要求更高,因為需要儲存三張影象。
-
該技術不能用於對大量半透明,玻璃狀的表面進行排序(或橫跨大部分螢幕的模糊alpha梯度),可能會產生失真。