【GPU精粹與Shader編程】(八) 《GPU Pro 1》全書核心內容提煉總結

 

        本文由@淺墨_毛星雲 出品,首發於知乎專欄,轉載請註明出處  

        文章鏈接: https://zhuanlan.zhihu.com/p/47866903

 

本文是【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端查看業界大牛們寫的代碼,鏈接如下:

https://github.com/QianMo/GPU-Pro-Books-Source-Code

 

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 ;

關於《孢子》更多的風格化渲染的教程,可以在這裏找到:

http://www.spore.com/comm/tutorials

 

二、《狂野西部:生死同盟》中的渲染技術 | 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 ;
}

圖 輪廓渲染算法的運行效果圖,輪廓剪影的實時生成和紋理化。

完整的實現Shader源碼可見: https://github.com/QianMo/GPU-Pro-Books-Source-Code/blob/master/GPU-Pro-1/03_Rendering%20Techniques/02_NPReffectsusingtheGeometryShader/NPRGS/NPRGS/Silhouette.fx

 

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;
}

最終的渲染效果:

圖 鉛筆渲染效果圖

完整的實現Shader源碼可見: https://github.com/QianMo/GPU-Pro-Books-Source-Code/blob/master/GPU-Pro-1/03_Rendering%20Techniques/02_NPReffectsusingtheGeometryShader/NPRGS/NPRGS/Pencil.fx

 

 

七、後處理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梯度),可能會產生失真。

 

7.1 核心實現Shader代碼

 

最終後處理合成的像素着色器實現代碼:

sampler2D rtMask : register (s0);
sampler2D rtOpaque : register (s1);
sampler2D rtFoliage : register (s2);
half maskLerp : register (c0); // 0.85h
half4 main(float2 texCoord : TEXCOORD0) : COLOR
{
    half4 maskPixel = tex2D ( rtMask , texCoord );
    half4 opaquePixel = tex2D ( rtOpaque , texCoord );
    half4 foliagePixel = tex2D (rtFoliage , texCoord );
    half mask = lerp(maskPixel .x , maskPixel .w, maskLerp );
    return lerp(opaquePixel , foliagePixel , mask * mask);
}

 

 

八、虛擬紋理映射簡介 | Virtual Texture Mapping 101

 

在這篇文章主要探討了如何實現一個功能完備的虛擬紋理映射(Virtual Texture Mapping,VTM)系統。

首先,虛擬紋理映射(VTM)是一種將紋理所需的圖形內存量減少到僅取決於屏幕分辨率的技術:對於給定的視點,我們只將紋理的可見部分保留在圖形存儲器中適當的MIP映射級別上,如下圖。

圖 使用單個的虛擬紋理渲染出獨特的紋理地形

早期的紋理管理方案是針對單個大紋理設計的[Tanner et al. 98],文章發表期間的VTM系統則更加靈活,模仿了操作系統的虛擬內存管理的思路:將紋理分成小的圖塊(tiles),或者頁(pages)[Kraus and Ertl 02, Lefebvre et al.04]。這些會根據渲染當前視點的需要自動緩存並加載到GPU上。但是,有必要將對缺失數據的訪問重定向(redirect)到後備紋理。這可以防止渲染中出現“空洞”(加載請求完成前的阻塞和等待的情況)。

文中的實現的靈感來源於GDC上Sean Barrett[Barret 08]的演講。如下圖所示,在每幀開始,先確定哪些圖塊(tiles)可見,接着識別出其中沒有緩存且沒有磁盤請求的圖塊。在圖塊上傳到GPU上的圖塊緩存之後,更新一個間接紋理(indrection texture,)或者頁表(page table)。最終,渲染場景,對間接紋理執行初始查找,以確定在圖塊緩存中採樣的位置。

圖 渲染圖塊ID,然後識別並更新最近可見的圖塊到圖塊緩存中(圖中的紅色),並可能會覆蓋不再可見的圖塊(圖中的藍色)。更新間接紋理並渲染紋理化表面(texturized surfaces)

間接紋理(indirection texture)是完整虛擬紋理的縮小版本,其中每個紋素都指向圖塊緩存(tile cache)中的圖塊。在文中的示例中,圖塊緩存只是GPU上的一個大紋理,包含小的,相同分辨率的正方形圖塊。

這意味着來自不同mip map級別的圖塊(tiles)會覆蓋虛擬紋理的不同大小區域,但會大大簡化圖塊緩存的管理。

 

 

8.1 核心實現Shader代碼

 

8.1.1 MIP 貼圖計算的Shader實現 | MIP Map Calculation

float ComputeMipMapLevel(float2 UV_pixels , float scale)
{
    float2 x_deriv = ddx (UV_pixels );
    float2 y_deriv = ddy (UV_pixels );
    float d = max (length (x_deriv ), length (y_deriv ));
    return max (log2(d) - log2(scale ), 0);
}

8.1.2 圖塊ID 的Shader實現 | Tile ID Shader

float2 UV_pixels = In.UV * VTMResolution ,

float mipLevel = ComputeMipMapLevel(UV_pixels , subSampleFactor);
mipLevel = floor(min (mipLevel , MaxMipMapLevel));

float4 tileID ;
tileID .rg = floor (UV_pixels / (TileRes * exp2(mipLevel )));
tileID .b = mipLevel ;
tileID .a = TextureID ;

return tileID ;

8.1.3 虛擬紋理查找的Shader實現 | Virtual Texture Lookup

float3 tileEntry = IndTex .Sample (PointSampler , In.UV);
float actualResolution = exp2(tileEntry .z);
float2 offset = frac(In.UV * actualResolution) * TileRes ;
float scale = actualResolution * TileRes ;
float2 ddx_correct = ddx (In.UV) * scale;
float2 ddy_correct = ddy (In.UV) * scale;
return TileCache .SampleGrad (TextureSampler ,
tileEntry .xy + offset ,
ddx_correct ,
ddy_correct );

 

 

Part III、全局光照 Global Illumination

 

九、基於間接光照的快速,基於模板的多分辨率潑濺 Fast, Stencil-Based Multiresolution Splatting for Indirect Illumination

 

本章介紹了交互式即時輻射度(radiosity)解決方案的改進,該解決方案通過使用多分辨率潑濺(multiresolution splats)技術,顯着降低了填充率(fill rate),並展示了其使用模板緩衝(stencil buffer)的一種高效實現。與最原始的多分辨率潑濺[Nichols and Wyman 09]不同的是,此實現不通過幾何着色器執行放大,因此能保持在GPU快速路徑(GPU fast path)上。相反,這章利用了GPU的分層模板剔除(hierarchical stencil culling)和Z剔除(z culling)功能,以在合適的分辨率下高效地進行光照的渲染。

其核心實現算法如下:

pixels ←FullScreenQuad();
vpls ← SampledVirtualPointLights();
for all ( v ∈ vpls ) do
    for all ( p ∈ pixels ) do
        if ( FailsEarlyStencilTest( p ) ) then
            continue; // Not part of multiresolution splat
        end if
        IlluminatePatchFromPointLight( p, v );
    end for
end for

圖 多分辨率光照潑濺開始於直接光照的渲染(左圖)。每個VPL產生一個全屏幕的圖示,允許每個VPL爲每個像素提供光線。每個VPL產生一個全屏的潑濺,允許每個VPL爲每個像素提供光線。但根據本地照明變化的速度,這些圖層會以多種分辨率呈現。僞彩色全屏潑濺(中圖)顯示了不同分辨率的區域,這些區域被渲染爲不同的buffer(右圖)

圖 多分辨率片的迭代求精從統一的粗圖像採樣開始(例如,162個採樣)。處理粗粒度片元,識別需要進一步求精的片元並創建四個更精細的分辨率片元。進一步的操作會進一步細化片元直到達到某個閾值,例如最大精度級別或超過指定的片元數量。

圖 前一幅圖中的,多分辨率潑濺可以進行並行計算而不是迭代計算(左圖)。右圖中的多分辨率buffer中的片元,都爲並行處理。

 

9.1 實現思路小結

 

9.1.1 多分辨率潑濺的實現思路 | Multiresolution Splatting Implement

以下是多分辨率潑濺實現思路,其中VPL的全稱是虛擬點光源(virtual point light):

1.	Render a shadow map augmented by position, normal, and color.
2.	Select VPLs from this shadow map.
3.	Render from the eye using only direct light.
4.	For each VPL:
    (a)	Draw a full-screen “splat.”
    (b)	Each splat fragment illuminates one texel (in one of the multiresolu- tion buffers in Figure 1.4) from a single VPL, though this texel may ultimately affect multiple pixels.
    (c)	Blend each fragment into the appropriate multiresolution buffer.
5.	Combine, upsample and interpolate the multiresolution buffers.
6.	Combine the interpolated illumination with the direct light.

9.1.2 設置可接受模糊 | Setting acceptable blur

patches ← CoarseImageSampling();
    for (i=1 to numRefinementPasses) do
        for all (p ∈ patches) do
        if ( NoDiscontinuity( p ) ) then
            continue;
        end if
        patches ← (patches − {p});
        patches ← (patches ∪ SubdivideIntoFour( p ) );
    end for
end for

9.1.3 從虛擬點光源收集光照進行潑濺 | Gathering illumination from VPLs for splatting

patches ← IterativelyRefinedPatches();
vpls ← SampledVirtualPointLights();
for all ( v ∈ vpls ) do
    for all ( p ∈ patches ) do
        TransformToFragmentInMultiresBuffer( p ); // In vertex shader
        IlluminateFragmentFromPointLight( p, v ); // In fragment shader
        BlendFragmentIntoMultiresBufferIllumination( p );
    end for
end for

9.1.4 降採樣多分辨率照明緩存 | Unsampling the multiresolution illumination buffer.

coarserImage ← CoarseBlackImage();
for all ( buffer resolutions j from coarse to fine ) do
    finerImage ← MultresBuffer( level j );
    for all ( pixels p ∈ finerImage ) do
        if ( InvalidTexel( p, coarserImage ) ) then
            continue; // Nothing to blend from lower resolution!
        end if
        p1, p2, p3, p4 ← FourNearestCoarseTexels( p, coarserImage );
        ω1, ω2, ω3, ω4 ← BilinearInterpolationWeights( p, p1, p2, p3, p4);
        for all ( i ∈ [1..4] ) do
            ωi = InvalidTexel( pi, coarserImage ) ) ? 0 : ωi;
        end for
        finerImage[p] += (ω1p1 + ω2p2 + ω3p3 + ω4p4)/(ω1 + ω2 + ω3 + ω4)
    end for
    coarserImage ← finerImage;
end for

9.1.5 並行潑濺求精 | Parallel splat refinement

for all (fragments f ∈ image) do
    if ( _ j such that f ∈ MipmapLevel( j ) ) then
        continue; // Fragment not actually in multires buffer
    end if
    j ← GetMipmapLevel( f );
    if ( IsDiscontinuity( f, j ) ) then
        continue; // Fragment needs further subdivision
    end if
    if ( NoDiscontinuity( f, j + 1 ) ) then
        continue; // Coarser fragment did not need subdivision
    end if
    SetStencil( f );
end for

9.1.6 最終基於模板的多分辨率潑濺算法

pixels ←FullScreenQuad();
vpls ← SampledVirtualPointLights();
for all ( v ∈ vpls ) do
    for all ( p ∈ pixels ) do
        if ( FailsEarlyStencilTest( p ) ) then
            continue; // Not part of multiresolution splat
        end if
        IlluminatePatchFromPointLight( p, v );
    end for
end for

 

 

十、屏幕空間定向環境光遮蔽 Screen-Space Directional Occlusion (SSDO)

 

環境光遮蔽(AO)是全局光照的一種近似,由於其良好的視覺質量和簡單的實現[Landis 02],其常常用於電影和遊戲中。環境光遮蔽的基本思想是預先計算網格表面幾個位置的平均可見性值。然後這些值在運行時與圖形硬件提供的未遮擋光照相乘。

環境光遮蔽的一個缺點是它僅適用於靜態場景。如果爲每個頂點或紋理元素預先計算了可見性值,則在網格變形時這些值將無效。

動態場景的一些初步想法有[Bunnell 06]和[Hoberock and Jia07]通過用層次圓盤(hierarchy of discs)近似幾何體的思路。處理動態場景的最簡單方法是根據幀緩衝區中的信息計算環境光遮蔽,即所謂的屏幕空間環境光遮蔽(SSAO)。這裏深度緩衝區用於在運行時計算平均可見度值而不是預先計算。這章內容發表期間的GPU算力已足以實時計算SSAO。此外,該方法不需要場景的任何特殊幾何的表現,因爲僅使用到幀緩衝器中的信息來計算遮蔽值。甚至不需要使用由多邊形組成的三維模型,因爲我們可以從產生深度緩衝區的任何渲染計算遮擋。

 圖 屏幕空間環境光遮蔽(SSAO):對於幀緩衝器中的每個像素,檢查一組相鄰像素,並將一個極小的球狀物體放置在相應的三維位置。爲每個球體計算遮蔽值,並將所有這些值累積到一個環境遮蔽值中。最後,該值乘以來自所有方向的未被遮蔽的光照。

環境光遮蔽通常顯示空腔暗化(darkening of cavities)和接觸陰影(contact shadows),但忽略入射光的所有方向信息。發生這種情況是因爲只有幾何體用於計算環境光遮蔽,而忽略了實際光照。典型的問題情況如下圖所示:在方向變化的入射光的情況下,環境光遮蔽將顯示錯誤的顏色。因此,該章將SSAO擴展到稱之爲屏幕空間定向遮擋(SSDO)的更真實光照技術。

由於循環遍歷片段程序中的許多相鄰像素,因此可以爲每個像素計算單獨的可見性值,而不是將所有信息摺疊爲單個AO值。因此,基本思想是使用來自每個方向的入射光的可見性信息,並僅從可見方向照射,從而產生定向的光照。

爲對SSDO的數據做進一步描述,假設有一個深度幀緩衝區,其中包含每像素的位置,法線和反射率值。

圖 環境光遮蔽的典型問題示例。由於紅色光源被遮擋而綠色光源照亮了點P,我們希望在這裏看到一個綠色的陰影。但環境遮擋首先計算來自所有方向的光照,因此點P最初爲黃色,然後通過某個平均遮擋值進行縮放,從而產生了不正確的棕色。

本章提出的SSDO算法具體可以總結如下:

  • 首先,在像素的三維點周圍放置一個半球,該半球沿着表面法線定向。該半球的半徑r_max是用戶參數,其用於決定搜索阻擋物的本地鄰域的大小。

  • 然後,將一些三維採樣點均勻分佈在半球內部。同樣,採樣點數N是用於時間質量平衡的用戶參數。

  • 接着,測試每個採樣方向的光照是否被阻擋或可見。因此,我們將每個採樣點反投影到深度幀緩衝區。在像素位置,可以讀取表面上的三維位置,並將每個點移動到表面上。如果採樣點朝向觀察者移動,則它最初位於表面下方並且被分類爲被遮擋。如果它遠離觀察者,它最初在表面上方並且被分類爲可見。

在下圖的示例中,點A,B和D在表面下方並被分類爲遮擋物。只有樣本C可見,因爲它在表面上方。因此,僅從方向C計算光照。

圖 SSDO屏幕空間定向環境光遮蔽。左圖:爲了計算點P處的方向遮擋,在半球中創建一些均勻分佈的採樣點,並將它們反投影到深度幀緩衝區中。(最初)在表面下方的每個點被視爲遮擋物。 右圖:僅從可見點計算光照。在這裏,假設每個採樣方向的立體角,並使用模糊環境貼圖傳入光亮度。

圖 屏幕空間定向環境光遮蔽(Screen-Space Directional Occlusion,SSDO)效果圖

 

10.1 核心實現Shader代碼

 

10.1.1 屏幕空間定向環境光遮蔽SSDO 的Shader源碼

// Read position and normal of the pixel from deep framebuffer .
vec4 position = texelFetch2D(positionTexture ,
ivec2 ( gl_FragCoord.xy), 0);
vec3 normal = texelFetch2D(normalTexture ,
ivec2( gl_FragCoord.xy), 0);

// Skip pixels without geometry .
if(position .a > 0.0) {
    vec3 directRadianceSum = vec3 (0.0);
    vec3 occluderRadianceSum = vec3 (0.0);
    vec3 ambientRadianceSum = vec3 (0.0);
    float ambientOcclusion = 0.0;

    // Compute a matrix that transform from the unit hemisphere .
    // along z = -1 to the local frame along this normal
    mat3 localMatrix = computeTripodMatrix(normal );

    // Compute the index of the current pattern .
    // We use one out of patternSize * patternSize
    // pre -defined unit hemisphere patterns (seedTexture ).
    // The i’th pixel in every sub -rectangle uses always
    // the same i’th sub -pattern .
    int patternIndex = int (gl_FragCoord.x) % patternSize +
    (int ( gl_FragCoord.y) % patternSize) *
    patternSize ;

    // Loop over all samples from the current pattern .
    for (int i = 0; i < sampleCount ; i++) {

        // Get the i’th sample direction from the row at
        // patternIndex and transfrom it to local space .
        vec3 sample = localMatrix * texelFetch2D(seedTexture ,
        ivec2(i, patternIndex), 0). rgb ;
        vec3 normalizedSample = normalize (sample );

        // Go sample -radius steps along the sample direction ,
        // starting at the current pixel world space location .
        vec4 worldSampleOccluderPosition = position +
        sampleRadius * vec4(sample .x, sample .y , sample .z, 0);

        // Project this world occluder position in the current
        // eye space using the modelview -projection matrix .
        // Due to the deferred shading , the standard OpenGL
        // matrix can not be used.
        vec4 occluderSamplePosition = (projectionMatrix *
        modelviewMatrix) * worldSampleOccluderPosition ;

        // Compute the pixel position of the occluder :
        // Do a division by w first (perspective projection ),
        // then scale /bias by 0.5 to transform [-1,1] -> [0 ,1].
        // Finally scale by the texure resolution .
        vec2 occluderTexCoord = textureSize2D(positionTexture ,0)
        * (vec2 (0.5) + 0.5 * ( occluderSamplePosition .xy /
        occluderSamplePosition .w));

        // Read the occluder position and the occluder normal
        // at the occluder texture coordinate .
        vec4 occluderPosition = texelFetch2D(positionTexture ,
        ivec2 (occluderTexCoord), 0);
        vec3 occluderNormal = texelFetch2D(normalTexture ,
        ivec2 (occluderTexCoord), 0);
        float depth = (modelviewMatrix *
        worldSampleOccluderPosition ).z;

        // Compute depth of corresponding (proj.) pixel position .
        float sampleDepth = (modelviewMatrix *
        occluderPosition).z + depthBias ;

        // Ignore samples that move more than a
        // certain distance due to the projection
        // (typically singularity is set to hemisphere radius ).
        float distanceTerm = abs (depth - sampleDepth) <
        singularity ? 1.0 : 0.0;

        // Compute visibility when sample moves towards viewer .
        // We look along the -z axis , so sampleDepth is
        // larger than depth in this case.
        float visibility = 1.0 - strength *
        (sampleDepth > depth ? 1.0 : 0.0) * distanceTerm;

        // Geometric term of the current pixel towards the
        // current sample direction
        float receiverGeometricTerm = max (0.0,
        dot (normalizedSample , normal ));

        // Compute spherical coordinates (theta , phi )
        // of current sample direction .
        float theta = acos(normalizedSample.y);
        float phi = atan( normalizedSample.z ,normalizedSample.x);
        if (phi < 0) phi += 2*PI;

        // Get environment radiance of this direction from
        // blurred lat /long environment map .
        vec3 senderRadiance = texture2D (envmapTexture ,
        vec2( phi / (2.0* PI), 1.0 - theta / PI ) ).rgb ;

        // Compute radiance as the usual triple product
        // of visibility , radiance , and BRDF.
        // For practical reasons , we post -multiply
        // with the diffuse reflectance color.
        vec3 radiance = visibility * receiverGeometricTerm *
        senderRadiance;

        // Accumulate the radiance from all samples .
        directRadianceSum += radiance ;
        // Indirect light can be computed here
        // (see Indirect Light Listing )
        // The sum of the indirect light is stored
        // in occluderRadianceSum
    }

    // In case of a large value of -strength , the summed
    // radiance can become negative , so we clamp to zero here.
    directRadianceSum = max (vec3(0), directRadianceSum);
    occluderRadianceSum = max (vec3 (0), occluderRadianceSum );

    // Add direct and indirect radiance .
    vec3 radianceSum = directRadianceSum + occluderRadianceSum;

    // Multiply by solid angle and output result .
    radianceSum *= 2.0 * PI / sampleCount ;
    gl_FragColor = vec4(radianceSum , 1.0);
    } else {

    // In case we came across an invalid deferred pixel
    gl_FragColor = vec4 (0.0);
}

 

10.1.2 SSDO間接光計算Shader實現代碼

間接光計算的源代碼。 此時,從SSDO計算中已知像素位置和遮擋物位置/紋理座標。這段代碼可以包含在上述SSDO實現代碼的循環結尾處。

// Read the (sender ) radiance of the occluder .
vec3 directRadiance = texelFetch2D(directRadianceTexture ,
ivec2( occluderTexCoord), 0);

// At this point we already know the occluder position and
// normal from the SSDO computation . Now we compute the distance
// vector between sender and receiver .
vec3 delta = position .xyz - occluderPosition.xyz ;
vec3 normalizedDelta = normalize (delta );

// Compute the geometric term (the formfactor ).
float unclampedBounceGeometricTerm =
max (0.0, dot (normalizedDelta , -normal )) *
max (0.0, dot (normalizedDelta , occluderNormal)) /
dot (delta , delta );

// Clamp geometric term to avoid problems with close occluders .
float bounceGeometricTerm = min (unclampedBounceGeometricTerm ,
bounceSingularity);

// Compute the radiance at the receiver .
vec3 occluderRadiance = bounceStrength * directRadiance *
bounceGeometricTerm ;

// Finally , add the indirect light to the sum of indirect light .
occluderRadianceSum += occluderRadiance;

 

 

十一、基於幾何替代物技術的實時多級光線追蹤 | Real-Time Multi-Bounce Ray-Tracing with Geometry Impostors

 

 

在實時應用中渲染反射和折射物體或它們的焦散(caustics)是一個具有挑戰性的問題。其需要非局部着色,這對於光柵化渲染管線來說比較複雜,其中片段着色器只能使用局部插值頂點數據和紋理來查找曲面點的顏色。

物體的反射、折射和其焦散效果通常需要光線跟蹤進行渲染,但光線跟蹤通常不具備與光柵化渲染相同的性能。

而通常,使用基於紋理的特殊tricks可以將光線跟蹤效果加入到實時場景中。這些技術通常假設場景中只有一個反射或折射物體,並且僅考慮一次或兩次反射光就足夠了。在這章中,遵循了類似的實踐原理,但是除去這些限制,以便能夠渲染布滿玻璃碎片的完整棋盤,甚至折射物體浸沒在動畫液體中等場景。

這章中擴展了先前基於環境距離替代物技術(environment distance impostors)的近似光線追蹤技術,以便在當時硬件條件的限制下,實時渲染具有多個反射和折射物體的場景。

有兩個關鍵的思路。

首先,文章改爲使用距離替代物(distance impostor)方法,不將內部光線(internal rays)與封閉的環境幾何體相交,而是將外部光線(external rays)與物體相交。另外,這章展示瞭如何高效地追蹤二次反射和折射光線,還研究了可以適應相同的任務的其他類型的幾何替代物技術 – 如幾何圖像(geometry images)[Carr et al. 06]和高度場(height fields)[Oliveira et al. 00, Policarpo et al. 05]。

第二個思路是靜態和動態對象的分離。經典的距離替代物(distance impostors)技術可以用於靜態環境,只需要在每一幀中更新移動對象的環境替代物(environment impostors)。通過搜索幾何替代物(geometry impostors)可以找到穿過移動物體的光路。

圖(a)環境距離替代物技術(environment distance impostor)(b)具有搜索投影前兩步策略的物體距離替代物(Object distance impostor)

 圖 左:整個測試場景。 右:使用高度圖替代物進行雙折射。

這章中擴展了先前基於環境距離替代物技術(environment distance impostors)的近似光線追蹤技術,以便在當時硬件條件的限制下,實時渲染具有多個反射和折射物體的場景。

當然,隨着技術的發展,2018年已經有了RTX技術,實時光線追蹤已經不在話下。以下便是一個能展現實時光線追蹤魅力的NVIDIA RTX Demo:

https://www.youtube.com/watch?v=KJRZTkttgLw

 

 

Part IV. 圖像空間 Image Space

 

 

十二、 GPU上的各項異性的Kuwahara濾波 | Anisotropic Kuwahara Filtering on the GPU

 

這章中介紹一種各向異性的Kuwahara濾波器[Kyprianidis et al. 09]。各向異性的Kuwahara濾波器是Kuwahara濾波器的一種廣義上的變體,通過調整濾波器的形狀,比例和方向以適應輸入的局部結構,從而避免了失真。由於這種適應性,定向圖像特徵被更好地保存和強調,得到了整體更清晰的邊緣和更具特色的繪畫效果。

圖 原始圖像(左),對各向異性的Kuwahara濾波輸出(右)。沿着局部特徵方向產生繪畫般的增強效果,同時保留形狀邊界。

 

12.1 Kuwahara濾波器(Kuwahara Filtering)

 

Kuwahara濾波器背後的一般思想是將濾波器內核分成四個重疊一個像素的矩形子區域。濾波器的響應由具有最小方差的子區域的平均值來定義。

圖 Kuwahara濾波器將濾波器內核分成四個矩形子區域。然後過濾器響應由具有最小方差的子區域的平均值來定義

圖 Kuwahara濾波器的輸出效果圖

 

12.2 廣義Kuwahara濾波器(Generalized Kuwahara Filtering)

 

而廣義Kuwahara濾波器,爲了克服不穩定次區域選擇過程的侷限性,定義了一個新的標準。結果被定義爲次區域平均值的加權總和,而不是選擇一個單獨的次區域。權重是根據子區域的差異來定義的。 這導致區域邊界更平滑並且失真更少。爲了進一步改善這一點,矩形子區域被扇區上的平滑權重函數所取代:

圖 廣義的Kuwahara濾波器使用定義在光盤扇區上的加權函數。濾波過濾器響應被定義爲局部平均值的加權總和,其中對具有低標準偏差的那些平均值賦予更多的權重。

圖 廣義Kuwahara濾波器的輸出效果圖

 

12.3 各向異性Kuwahara濾波器(Anisotropic Kuwahara Filtering)

 

廣義的Kuwahara濾波器未能捕獲定向特徵並會導致集羣的失真。而各向異性的Kuwahara濾波器通過使濾波器適應輸入的局部結構來解決這些問題。在均勻區域中,濾波器的形狀應該是一個圓形,而在各向異性區域中,濾波器應該變成一個橢圓形,其長軸與圖像特徵的主方向一致。

圖 各向異性Kuwahara濾波器圖示

 

十三、基於後處理的邊緣抗鋸齒 | Edge Anti-aliasing by Post-Processing

 

抗鋸齒是高質量渲染的關鍵之一。例如,高質量的CG優先考慮抗鋸齒的質量,而用於打印和品宣的遊戲截圖通常會採用人爲高水平的超級採樣來提高圖像質量。

硬件多采樣抗鋸齒(Multi-Sampled Anti-Aliasing ,MSAA) [Kirkland 99]支持的是實現抗鋸齒的標準方法,但是它是實現高質量抗鋸齒的一種非常昂貴的方式,並且對抗鋸齒與後處理效果提供的幫助甚微。

本章介紹了一種通過選擇性像素混合對邊緣進行抗鋸齒的新方法。其僅需要MSAA所需空間的一小部分,並且與後處理效果相兼容。此抗鋸齒方法的執行分爲兩個階段。

首先,圖像是沒有任何多采樣(multisampling)方法或超採樣(super-sampling)方法的作用下渲染的。作爲關於鄰近邊緣輪廓的近似細小的渲染提示被寫出到幀緩衝區。然後應用後處理的pass,該通道使用這些細小的渲染提示來更新邊緣像素,以提供抗鋸齒。而在延遲效果(deferred effects)之後應用後處理(post-process),表示它們會接收邊緣消除鋸齒。

這種方法的核心部分爲像素着色器提供了一種計算最近輪廓邊緣位置的高效方法。這種技術也可以應用於陰影貼圖放大,並提供了保持銳利邊緣的放大方法。

下圖顯示了該方法的實際應用。

圖 本章中的抗鋸齒方法的效果圖特寫

圖 複雜背景的抗鋸齒效果演示。每個放大部分的左側爲4 MSAA的抗鋸齒效果。右側爲本章方法(edge-blur render,邊緣模糊抗鋸齒)

 

十四、基於Floyd-Steinberg半色調的環境映射 | Environment Mapping with Floyd-Steinberg Halftoning

 

這章中提出了一種使用GPU計算重要性採樣的算法。該算法巧妙地應用了經典的半色調技術,可用於加速高質量環境映射照明中的重要性採樣步驟。

這章想傳達的最重要的信息是半色調(halftoning)算法和重要性採樣(importance sampling)是等價的,因此我們可以在重要性採樣中使用半色調算法。文中研究了Floyd-Steinberg半色調方法在環境映射中的應用,並得出結論認爲,該方法可以比隨機抽樣更好地對樣本進行分配,所以,對的樣本計算的積分也會更準確。

圖 左圖爲隨機採樣加權環境貼圖(Sampling weighted environment maps);右圖爲弗洛伊德 - 斯坦伯格採樣半色調環境映射(Floyd-Steinberg halftoning)

圖 光源採樣結果。隨機採樣基於Floyd-Steinberg半色調映射通過方向光源對兔子模型的漫反射和鏡面光照。

 

14.1 核心實現Shader代碼

 

在幾何着色器中實現的Floyd-Steinberg採樣器Shader代碼:

[maxvertexcount (32)]
void gsSampler ( inout PointStream <float4 > samples ) {
    uint M = 32; float S = 0;
        [loop]for (uint v = 0; v < R.y; v++)
            [loop]for (uint u = 0; u < R.x; u++)
                S += getImportance(uint2(u, v));
    float threshold = S / 2 / M;
    float4 cRow[RX4 ]={{0,0,0,0},{0 ,0 ,0 ,0},{0 ,0,0,0} ,{0,0,0 ,0}};
    float cPixel = 0, cDiagonal = 0, acc [4];
    [loop]for (uint j = 0; j < R.y; j++) {
        uint kper4 = 0;
        [loop]for (uint k = 0; k < R.x; k += 4) {
        for (uint xi = 0; xi < 4; xi++) {
            float I = getImportance(uint2 (k+xi , j));
            float Ip = I + cRow[kper4 ][xi] + cPixel ;
            if(Ip > threshold) {
                float3 dir = getSampleDir(uint2 (k+xi , j));
                samples .Append ( float4 (dir , I / S) );
                Ip -= threshold * 2;
            }
            acc [xi] = Ip * 0.375 + cDiagonal ;
            cPixel = Ip * 0.375;
            cDiagonal = Ip * 0.25;
        }
        cRow[kper4 ++] = float4 (acc [0], acc [1], acc [2], acc [3]);
    }
    j++; kper4 --;
    [loop]for (int k = R.x -5; k >= 0; k -= 4) {
        for (int xi = 3; xi >= 0; xi--) {
            float I = getImportance(uint2 (k+xi , j));
            float Ip = I + cRow[kper4 ][xi] + cPixel ;
            if(Ip > threshold ) {
                float3 dir = getSampleDir(uint2 (k+xi , j));
                samples .Append ( float4 (dir , I / S) );
                Ip -= threshold * 2;
            }
            acc [xi] = Ip * 0.375 + cDiagonal ;
            cPixel = Ip * 0.375;
            cDiagonal = Ip * 0.25;
        }
        cRow[kper4 --] = float4 (acc [0], acc [1], acc [2], acc [3]);
        }
    }
}

 

 

十五、用於粒狀遮擋剔除的分層項緩衝 | Hierarchical Item Buffers for Granular Occlusion Culling

 

剔除(Culling)算法是許多高效的交互式渲染技術的關鍵,所有剔除算法的共同目標都是從渲染管線的幾乎所有階段減少工作量。

最常用的算法是在應用階段採用視錐剔除(frustum culling)和視口剔除(portal culling)來排除不可見的幾何體,通常按層次數據結構組織。更復雜的算法會在昂貴的前期流程中預計算整個可見集,以實現高效的實時可見性計算。

這章提出了一種直接在GPU上運行的剔除方法,該方法對應用程序完全透明且實現起來非常簡單,特別是在下一代硬件和圖形API上。文中表明,只需很少的開銷,每幀的渲染時間可以顯着減少,特別是對於昂貴的着色器或昂貴的渲染技術。該方法特別針對像幾何着色器這樣的早期着色器階段,且應用目標是多方面。例如,[Engelhardt and Dachsbacher 09]展示了這種技術的應用,以加速每像素位移映射,但它也爲基於可見性的LOD控制和曲面細分着色器中的剔除提供了可能性。

圖 分層項緩衝區(Hierarchical Item Buffers)圖示(a)確定可見度的實體;(b)光柵化後的項緩衝區(item buffer);(c)項緩衝區的直方圖。實體3沒有計算任何內容,因此是不可見的。

 

 

十六、後期製作中的真實景深 | Realistic Depth of Field in Postproduction

 

景深(Depth of field,DOF)是一種典型的攝影效果,其結果是根據攝像機與攝像機的距離而產生不同的聚焦區域。

這章中,提出了一種交互式GPU加速的景深實現方案,其擴展了現有方法的能力,具有自動邊緣改進和基於物理的參數。散焦效應通常由模糊半徑控制,但也可以由物理特性驅動。此技術支持在圖像和序列上使用灰度深度圖圖像和參數,如焦距,f-stop,subject magnitude,相機距離,以及圖像的實際深度。

另外,景深實現中額外的邊緣質量改進會產生更逼真和可信的圖像。而局部鄰域混合算法的缺點是二次計算能力,但這其實可以通過GPU進行補償。

圖 景深效果圖

圖 模擬曝光的光圈孔徑形狀示例

 

十七、實時屏幕空間的雲層光照 | Real-Time Screen Space Cloud Lighting

 

在創造逼真的虛擬環境時,雲是一個重要的視覺元素。實時渲染美麗的雲可能非常具有挑戰性,因爲雲在保持交互式幀率的同時會呈現出難以計算的多重散射(multiple scattering)。

目前的問題是,大多數遊戲都無法承擔精確計算物理上正確的雲層光照的計算成本。

本章介紹了一種可以實時渲染真實感的雲層的非常簡單的屏幕空間技術。這種技術已經在PS3上實現,並用於遊戲《大航海時代Online》(Uncharted Waters Online)中。這項技術並不關注嚴格的物理準確性,而是依靠重新創建雲層的經驗外觀。另外需要注意的是,此技術適用於地面場景,玩家可以在地面上觀看,並且只能從遠處觀看雲層。

光照是創造美麗和真實感雲彩最重要的方面之一。當太陽光穿過雲層時,被雲層中的粒子吸收,散射和反射。下圖展示了一個典型的戶外場景。

圖 一個典型的戶外場景。 最靠近太陽的雲顯示出最大的散射並且看起來最亮

如圖所示,從圖中所示的視圖看雲層時,最靠近太陽的雲顯得最亮。這種現象是由於太陽的光線到達雲層的後方,然後通過多次散射,在雲的前部(最靠近觀察者)重新出現。這一觀察結果是這章所介紹技術的關鍵部分。爲了再現這種視覺提示,屏幕空間中的簡單點模糊或方向模糊足以模仿通過雲層的光散射。

 

17.1 實現方案

 

這章的雲層渲染技術可以分爲三個pass執行:

  • 首先,渲染雲密度(cloud density)爲離屏渲染目標(RT),且雲密度是可以由藝術家繪製的標量值。

  • 其次,對密度貼圖(density map)進行模糊處理。

  • 最終,使用模糊的密度貼圖來渲染具有散射外觀的雲層。

圖 基於這章技術實現的demo截圖

在demo中,雲層被渲染爲一個統一的網格。 雲層紋理在每個通道中包含四個密度紋理。每個通道代表不同的雲層,根據第一個通道中的天氣在像素着色器中混合。並且也通過滾動紋理座標UV來實現動畫。

總之,這章提出了一種實時渲染的真實感天空的技術。由於雲的形狀與光源分離,程序化雲的生成和程序化動畫都可以支持。

需要注意的是,此方法忽略了大氣的某些物理特性,以創建更高效的技術。例如,不考慮大氣的密度,但這個屬性對於創造逼真的日落和日出是必要的。也忽略了進入雲層的光的顏色。在日落或日出的場景中,只有靠近太陽的區域應該明亮而鮮豔地點亮。有必要採取更基於物理的方法來模擬太陽和雲之間的散射,以獲得更自然的結果。

 

17.2 核心實現Shader代碼

 

以下爲雲層光照像素着色器核心代碼;注意,常數爲大寫。此着色器可以通過適當設置SCALE和OFFSET常量來提供平行線或點的模糊:

// Pixel shader input
struct SPSInput 
{
    float2 vUV : TEXCOORD0 ;
    float3 vWorldDir : TEXCOORD1 ;
    float2 vScreenPos : VPOS;
};

// Pixel shader
float4 main( SPSInput Input ) 
{
    // compute direction of blur.
    float2 vUVMove = Input .vScreenPos * SCALE + OFFSET ;

    // Scale blur vector considering distance from camera .
    float3 vcDir = normalize ( Input .vWorldDir );
    float fDistance = GetDistanceFromDir( vcDir );
    vUVMove *= UV_SCALE / fDistance ;

    // Limit blur vector length .
    float2 fRatio = abs ( vUVMove / MAX_LENGTH );
    float fMaxLen = max ( fRatio .x, fRatio .y );
    vUVMove *= fMaxLen > 1.0 f ? 1.0 f / fMaxLen : 1.0 f;

    // Compute offset for weight .
    // FALLOFF must be negative so that far pixels affect less.
    float fExpScale = dot ( vUVMove , vUVMove ) * FALLOFF ;

    // Blur density toward the light.
    float fShadow = tex2D ( sDensity , Input.vUV ).a;
    float fWeightSum = 1.0 f;
    for ( int i = 1; i < FILTER_RADIUS; ++i ) {
    float fWeight = exp ( fExpScale * i );
    fShadow +=
    fWeight * tex2D(sDensity , Input .vUV +vUVMove *i).a;
    fWeightSum += fWeight ;
    }
    fShadow /= fWeightSum ;

    // 0 means no shadow and 1 means all shadowed pixel .
    return fShadow ;
}

 

十八、屏幕空間次表面散射 | Screen-Space Subsurface Scattering

 

這章提出了一種能夠以後處理的方式,將渲染幀的深度-模板和顏色緩衝區作爲輸入,來模擬屏幕空間中的次表面散射的算法。此算法開創了皮膚渲染領域的基於屏幕空間的新流派。其具有非常簡單的實現,在性能,通用性和質量之間取得了很好的平衡。

圖 在紋理空間中執行模糊處理,如當前的實時次表面散射算法(上圖)所做的那樣,直接在屏幕空間完成模糊(下圖)

該算法轉換了從紋理到屏幕空間的擴散近似的計算思路。主要思想並不是計算輻照度圖並將其與擴散剖面進行卷積,而是將卷積直接應用於最終渲染圖像。下顯示了此算法的核心思想。

圖 屏幕空間次表面散射流程圖

需要注意的是,要在屏幕空間中執行此工作,需要輸入渲染該幀的深度模板和顏色緩衝區。

圖 屏幕空間次表面散射示例。與紋理空間方法不同,屏幕空間的方法可以很好地適應場景中的對象數量(上圖)。在不考慮次表面散射的情況下進行渲染會導致石頭般的外觀(左下圖);次表面散射技術用於創建更柔和的外觀,更能好地代表次表面散射效果(右下圖)。

本章提出的次表面散射算法當物體處於中等距離時,提供了與Hable等人的方法[Hable et al.09]類似的性能,並且隨着物體數量的增加能更好地勝任工作。且此方法更好地推廣到其他材質。在特寫鏡頭中,其確實需要用一些性能去換取更好的質量,但它能夠保持原來d'Eon方法的肉感(fleshiness)[d’Eon and Luebke 07]。但是,在這些特寫鏡頭中,玩家很可能會密切關注角色的臉部,因此值得花費額外的資源來爲角色的皮膚提供更好的渲染質量。

 

18.1 核心實現Shader代碼

 

進行水平高斯模糊的像素着色器代碼:

float width ;
float sssLevel , correction , maxdd;
float2 pixelSize ;
Texture2D colorTex , depthTex ;

float4 BlurPS (PassV2P input) : SV_TARGET {
    float w[7] = { 0.006 , 0.061 , 0.242 , 0.382 ,
    0.242 , 0.061 , 0.006 };

    float depth = depthTex .Sample (PointSampler ,
    input .texcoord ).r;
    float2 s_x = sssLevel / (depth + correction *
    min (abs (ddx (depth )), maxdd ));
    float2 finalWidth = s_x * width * pixelSize *
    float2 (1.0, 0.0);
    float2 offset = input .texcoord - finalWidth ;
    float4 color = float4 (0.0, 0.0, 0.0, 1.0);

    for (int i = 0; i < 7; i++) {
        float3 tap = colorTex .Sample (LinearSampler , offset ).rgb ;
        color .rgb += w[i] * tap ;
        offset += finalWidth / 3.0;
    }

    return color ;
}

 

Part V. 陰影 Shadows

 

陰影方面,作爲次核心章節,僅進行更精煉的小篇幅的總結。

 

十九、快速傳統陰影濾波 | Fast Conventional Shadow Filtering

 

這章介紹瞭如何減少常規陰影貼圖過濾的硬件加速百分比鄰近過濾(percentage closer filtering,PCF)紋理操作次數。現在只需要16次PCF操作就可以執行通常使用49次PCF紋理操作進行的均勻8×8過濾器。由於紋理操作的數量通常是傳統陰影濾波的限制因素,因此實現的加速效果比較顯著。PS:文中附帶了大量的shader實現源碼。

圖 效果截圖

 

二十、混合最小/最大基於平面的陰影貼圖 | Hybrid Min/Max Plane-Based Shadow Maps

 

這章介紹瞭如何從常規的深度陰影貼圖(depth-only shadow map)導出二次貼圖。這種二次紋理可以用來大大加快昂貴的陰影濾波與過大的過濾器性能佔用。它以原始陰影圖中二維像素塊的平面方程或最小/最大深度的形式存儲混合數據。,被稱爲混合最小/最大平面陰影貼圖(hybrid min/max plane shadow map,HPSM)。該技術特別適用於在大型過濾區域和前向渲染的情況下加速陰影過濾,例如,當陰影過濾成本隨着場景的深度複雜度而增加時。

圖 一個最小/最大陰影貼圖像素(即有噪聲的四邊形)可以映射到許多屏幕上的像素。

 

二十一、基於四面體映射實現全向光陰影映射 | Shadow Mapping for Omnidirectional Light Using Tetrahedron Mapping

陰影映射(Shadow mapping)是用於三維場景渲染陰影的一種常用方法。William的原始Z-緩衝器陰影映射算法〔[Williams 78]是用於方向光源的,需要一種不同的方法來實現全向光(Omnidirectional Light)的陰影。

有兩種流行的方法來接近全向光:一個是立方體映射(cube mapping )[Voorhies and Foran 94]和另一個是雙拋物面映射(dual-paraboloid mapping)[Heidrich and Seidel 98]。而在本章中,提出了一種全新的使用四面體映射(tetrahedron mapping)的全向光陰影映射技術。

圖 四個點光源,並使用四面體陰影映射與模板緩衝和硬件陰影映射得到渲染效果。二維深度紋理尺寸爲1024×1024

 

 

二十二、屏幕空間軟陰影 | Screen Space Soft Shadows

 

這章中提出了一種基於陰影映射的半影(penumbrae)實時陰影渲染的新技術。該方法使用了包含陰影與其潛在遮擋物之間距離的屏幕對齊紋理,其用於設置在屏幕空間中應用的各向異性高斯濾波器內核的大小,從而平滑標準陰影創建半影(penumbra)。考慮到高斯濾波器是可分離的,創建半影的樣本數量會遠低於其他軟陰影方法。因此,該方法獲得了更高的性能,同時也能得到外觀正確的半影。

圖 不同光源尺寸和不同光源顏色的半影的示例

 

The End.

下次更新,《GPU Pro 2》全書核心內容提煉總結,再見。

With best wishes.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章