根據作業需求進行實作。
Tip
以 Assignment 1 專案為基礎做修改與擴充。 皆為自己撰寫 (部分有詢問 AI 來學習,但主要由我自己撰寫),並無套用任何既成之 D3D12 渲染框架。
本專案以 Deferred Rendering 為基礎,將 Sponza 場景、PMX 人型角色、VMD 骨架動畫、Cel Shading、Directional Shadow Mapping、PCF、SSAO 與 ACES Tone Mapping 整合在同一個 DirectX 12 專案中。
目前主要的 Rendering Pipeline 如下:
Shadow Pass
↓
Geometry Pass
├─ Albedo G-Buffer
├─ Normal G-Buffer
└─ World Position G-Buffer
↓
SSAO Pass
↓
Lighting Pass
├─ Ambient / Directional / Point Light
├─ Cel Shading
├─ Shadow Mapping / PCF
├─ SSAO Composition
└─ ACES Tone Mapping
↓
HUD / Text Overlay
↓
Back Buffer
已完成的主要功能:
- PMX 模型、材質、貼圖、骨架與 Alpha-tested 區域讀取。
- VMD 骨架動畫讀取、骨架階層解析與 Linear Blend Skinning。
- 動畫角色的梯度漫反射、輪廓線與 Rim Light。
- Directional Light Shadow Mapping,可動態調整水平及垂直光線方向。
- Shadow Depth Bias、Slope-scaled Bias、Normal Offset Bias 與 Comparison Bias。
- 可切換 Hard Shadow 與 3×3 Percentage-Closer Filtering (PCF)。
- 16 Samples Hemisphere Kernel SSAO、4×4 Procedural Random Rotation 與 5×5 Blur。
- 可調整 SSAO Radius、Bias 與 Intensity。
- ACES Approximation Tone Mapping 與 Exposure 調整。
- Debug Render Modes:Depth、Normal、Albedo、Final Color、SSAO、Shadow。
- 即時 FPS 與操作資訊 HUD,可使用空白鍵開關。
Note
專案預設解析度為 1280 × 720。實際 FPS 會依照 GPU、攝影機位置與畫面中可見幾何量有所差異;Demo 時應在 PMX 動畫、Shadow、PCF、SSAO 與 Tone Mapping 全部開啟的情況下確認穩定維持 45 FPS 以上。
Note
互動的部分,有盡可能解耦獨立程式碼
Camera.h及Camera.cpp用來處理攝影機相關InputManager.h用來處理輸入相關- 這些檔案目前在架構上皆為 Singleton 存在,因為目前不會有複數的情況
- 也有整合進
Win32Application.cpp中 - 最後在
D3D12HelloTexture.cpp實作HandleInput()處理輸入
-
W/A/S/D:第一人稱移動 -
Q/E:Q下移;E上移 -
滑鼠:第一人稱轉視角- 程式一執行後,滑鼠會隱藏進入遊戲狀態,直接轉即可
- 為了方便實作,目前程式中僅使用 Euler Angles 實作旋轉
- 未來有機會時,應該實作更精準且不會卡死的 Quaternions 方法
- 攝影機的 Front Vector 計算如下,其中
$\psi$ 為 Yaw、$\theta$ 為 Pitch:
本專案的陰影主要使用直射光(Directional Light)實作。
J:開關直接光源影響場景渲染(關閉時僅有環境光 (Ambient))F:切換 Hard Shadow 1×1 與陰影邊緣模糊效果 3×3 PCFH/K:調整 Directional Light 的水平方向U/M:調整 Directional Light 的垂直方向
↑/↓:調整(+/-) Tone Mapping (使用 ACES 演算法) 的曝光度C:開關 Screen Space Ambient Occlusion (SSAO) 之效果[/]:調整(+/-)SSAO 的半徑;/':調整(+/-)SSAO 的 Bias 值,/.:調整(+/-)SSAO 的強度
AssimpLoader:開啟靜態場景(Sponza 模型)資源之方式,整合 Open Asset Import Library (Assimp) 來讀取模型與貼圖。PMXLoader:開啟 MMD 模型資源(二進位檔案(.PMX))之方式。包括讀取:模型 (Model) 檔案、貼圖 (Texture) 檔案、材質 (Material) 資訊、骨架 (Skeleton) 資訊。VMDLoader:開啟 MMD 之動作檔案 (.VMD) 之方式。MMDAnimator:將 VMD 動作檔綁到 PMX 模型檔上並播放。GeometryPass.hlsl與LightingPass.hlsl:處理 Cel / Toon Shading 之相關算法(包含梯度漫反射、輪廓線描邊與 Rim light),並成功套在動作上貼圖無破圖。- 會記錄模型是否為 PMX 角色,若是才套渲染。
- 使用 Normal G-Buffer 的 Alpha 通道記錄是否為 PMX 角色。
PMX 原始資料與本專案使用的 DirectX 左手座標系不同,因此讀取 Position、Normal 與 Bone Position 時會反轉 Z 軸:
為了維持三角形正確的正反面方向,Index 順序由:
改為:
目前 Loader 會處理 BDEF1、BDEF2、BDEF4、SDEF 與 QDEF 對應的 Bone Index / Bone Weight 資料;其中 SDEF 目前以 BDEF2 權重方式近似,尚未使用 SDEF 的額外曲面參數。
模型貼圖使用相對於 PMX 檔案所在資料夾的路徑。Geometry Pass 會依材質的 opacityMode 決定是否執行 Alpha Test:
當貼圖 Alpha 小於 0.5 時捨棄該 Pixel,用於角色頭髮、睫毛與其他 Cutout 區域。
VMD 骨架名稱使用 Shift-JIS,因此讀取後使用 Windows Code Page 932 轉換為 std::wstring。位置資料轉換為左手座標系時反轉 Z 軸;旋轉四元數則反轉 X 與 Y:
目前會讀取 Bone Motion,並依骨架名稱分組、依 Frame 編號排序。VMD 原始的 Bezier Interpolation 目前未實作,而是使用線性位置插值與 Quaternion Slerp。
兩個 Keyframe 之間的時間參數為:
位置使用 Linear Interpolation:
旋轉使用 Spherical Linear Interpolation:
每根 Bone 的 Local Bind Position 為 Bone Position 與 Parent Bone Position 的差:
Bone 的 Local Transform 由 Bind Translation、VMD Position Offset 與 VMD Rotation 組成:
本專案使用 Row Vector 的矩陣乘法方式,Global Transform 依骨架階層遞迴計算:
Root Bone 則為:
最終 Skinning Matrix 為:
其中 mul(rowVector, matrix) 的使用方式。
為避免模型中 Bone Weight 未正規化,Vertex Shader 會先計算:
頂點位置與法線分別使用最多四根骨架進行 Linear Blend Skinning:
完成 Skinning 後,再套用 PMX 模型的 Scale、Y 軸 Rotation 與 Translation,使角色能正確放入 Sponza 場景。
角色的梯度漫反射先以 Half-Lambert 計算:
為避免硬切 Threshold 在模型表面造成明顯的三角面色塊,使用 smoothstep 建立較平滑的 Toon Ramp:
角色仍會投射 Shadow 至 Sponza;為降低薄片衣物、頭髮與裙擺的 Self-shadow Artifact,Shadow Map 對角色本身的接收強度設定為 0.15:
輪廓線使用 Screen-space 的 Normal / Position 差異判斷。對目前 Pixel 的上下左右四個鄰居取樣:
當:
則將該 Pixel 判定為 Outline,輸出黑色。此方式不需要額外放大模型或繪製 Back-face Outline Pass,但輪廓寬度會受到解析度影響。
Rim Light 先使用 View Direction 與 Normal 計算輪廓強度:
再以四次方集中到模型邊緣,並限制在朝向光源的表面:
最後使用 smoothstep 形成柔和的 Rim Band:
沒有處理 Facial Animation、IK、裙擺/頭髮物理及 Camera Motion。
本專案建立一張 2048 × 2048 的 Directional Shadow Map。Shadow Pass 僅輸出 Depth,不輸出 Color;Sponza 與 PMX 角色皆會使用與 Geometry Pass 相同的 Model / Skinning Transform 寫入 Shadow Map。
為了避免透明頭髮、葉片或鏈條的透明區域寫入錯誤深度,Shadow Pass 亦使用 Diffuse Texture Alpha Test:
Directional Light 使用水平方位角
H / K 調整 U / M 調整
因此光線方向改變時,Shadow Map 會同步重新繪製。
目前 Pixel 的 World Position 轉換至 Light Clip Space:
Perspective Divide 後轉換至 Shadow Map UV:
若 Shadow Map 中記錄的深度比目前 Pixel 的 Light-space Depth 更靠近光源,表示目前 Pixel 被遮擋。
Caster 端的 Rasterizer Bias 設定為:
DepthBias = 300;
DepthBiasClamp = 0.0006f;
SlopeScaledDepthBias = 0.75f;Receiver 端則依 Normal 與 Light Direction 的夾角使用 Normal Offset:
角色使用的 Normal Offset 範圍為:
Sponza 使用:
除此之外,Depth Comparison 仍保留較小的 Comparison Bias:
角色的 0.00004 ~ 0.00014,場景則約為 0.00006 ~ 0.00025。Caster Bias、Normal Offset 與 Comparison Bias 共同降低 Shadow Acne,同時避免使用過大的單一 Bias 造成 Peter Panning。
可使用 F 鍵切換 Hard 1×1 Shadow 與 3×3 PCF。
Hard Shadow 僅進行一次 Depth Comparison:
PCF 則在 Shadow Map 周圍取 3×3 共九個 Sample,將每次比較結果平均:
其中:
3×3 Kernel 能降低 Shadow Edge 的鋸齒,同時避免過大的 Filter Radius 造成明顯的 Light Leaking。
Note
Point Light 目前僅作為額外場景照明,未實作 Cubemap Point Shadow;本專案的 Shadow Mapping 評分項目以 Directional Light 為主。
SSAO 使用 Geometry Pass 輸出的 World Position G-Buffer 與 Normal G-Buffer。Shader 中先將 Position 與 Normal 轉換至 View Space,再在表面法線為中心的 Hemisphere 中取樣。
為兼顧效能,SSAO 使用半解析度的 R8_UNORM Render Target,Sampling Kernel 數量為 16。
每個 Sample 初始為:
其中:
因為
其中
為避免每個 Pixel 使用相同 Kernel 方向而產生 Banding,Shader 使用重複的 4×4 Procedural Hash Pattern 產生隨機角度:
將 Random Vector 投影到表面 Tangent Plane:
Sample Direction 由 Tangent Space 轉換至 View Space:
每個 Sample Position 為:
再以 Projection Matrix 投影回 Screen UV,讀取該位置的 View-space Depth。DirectX 左手 View Space 中,較小的正 Z 值表示更接近 Camera,因此遮蔽判斷為:
為降低不相關表面與深度不連續造成的 Halo,若:
則捨棄該 Sample。其 Range Weight 為:
Raw AO 為:
Intensity 最後以冪次調整:
Lighting Pass 中對 SSAO Texture 使用 5×5 Box Blur:
Blur 能降低 Random Rotation 造成的 Noise。由於目前使用一般 Box Blur,而非 Bilateral Blur,在 Depth Discontinuity 或物件邊界附近仍可能出現輕微 Halo。
Radius:決定搜尋遮擋物的距離;數值越大,AO 影響範圍越廣。Bias:避免表面將自身誤判為遮擋物;數值過小容易產生 Self-occlusion,過大則會失去接觸陰影。Intensity:控制 AO 最終明暗強度,不改變搜尋範圍。C:Enable / Disable SSAO,方便比較前後效果。
SSAO 最終只影響 Ambient / Indirect Light,不直接將 Directional Direct Light 全部乘黑,以避免場景過度暗沉。
SSAO 為 Screen-space Technique,因此具有以下限制:
- 無法取得畫面外或被遮擋幾何的資訊。
- Depth Discontinuity 附近可能出現 Halo。
- 大 Radius 容易跨越不相關表面。
- Camera 移動時可能看到 Noise Pattern 變化。
- 一般 Blur 可能使 AO 跨越物件輪廓。
Lighting Pass 先計算 Linear-space Light Contribution,再乘上 Exposure:
本專案使用 ACES Approximation:
此公式分別套用於 RGB 三個 Channel。與直接 saturate / Clamp 相比,ACES 能將高亮區域平滑壓縮至顯示範圍,保留更多高亮層次。
Tone Mapping 後,再進行 Gamma Correction:
最後才將結果限制在
↑ / ↓ 會即時調整 Exposure,最低限制為 0.1。Exposure 越大,進入 ACES 前的線性亮度越高;但高亮區會由 ACES 曲線壓縮,而不是直接被截斷。
Note
目前 ACES 計算直接寫在 LightingPass.hlsl 的後段,Lighting Pass 直接輸出至 R8G8B8A8_UNORM Back Buffer。若要完全拆成標準 HDR Pipeline,可再新增 R16G16B16A16_FLOAT HDR Intermediate Render Target,將 Lighting 與 Tone Mapping 分成兩個獨立 Pass。
- Facial Animation。
- IK Solver。
- 裙擺 / 頭髮物理。
- VMD Camera Motion。
- Point Light Cubemap Shadow。
- Spot Light Shadow。
- Bloom。
- Bilateral SSAO Blur。
- 獨立 HDR Intermediate Render Target / Tone Mapping Pass。
- VMD Bezier Interpolation,目前使用 Lerp / Slerp。
- PMX SDEF 目前以 BDEF2 權重方式近似。