在 Day 24 中,我們解密了 THREE.Points 點光柵化效能、加法混合光學,並為地宮點綴了飛舞的火星餘燼與空氣懸浮微塵。
然而,當你在場景中架設好燈光與材質後,是否曾遇過這些令許多 WebGL 開發者抓狂的色彩災難:
畫面蒙上一層洗不掉的灰霧:石磚暗處沉不下去、高光處暗淡無光,整張畫面對比度極低,彷彿蓋了一張半透明灰塑膠布。
光源一拉高就「瞬間死白」:稍微把火把強度調大,中心光斑立即變成純白一片的死板硬塊,火燭溫潤的金黃層次蕩然無存。
加入後處理管線後顏色全變調:原本在原生 renderer.render 裡調好的漂亮色調,一換成 EffectComposer 就突然過暗或色彩失真。
這些並非你的 3D 模型或材質貼圖有瑕疵,而是觸碰到了數位圖形學中最核心、也最容易被誤解的領域——色彩空間(Color Space)與色調映射(Tone Mapping)。
今天我們將跳過生澀晦澀的光學儀器公式,用最直覺的邏輯,拆解為什麼電腦螢幕會「欺騙」你的眼睛,以及我們如何利用電影工業標準的 ACES 色彩曲線,調配出 HD-2D 標誌性的油畫級光影!
要搞懂為什麼畫面會「發灰」或「暴衝」,必須先認識 GPU 的大腦與人類的眼睛之間的根本矛盾。
在真實物理世界與 GPU 著色器中,光線運算完全遵循純粹的線性加減乘除:
人類的眼睛演化自大自然,在演化過程中,我們需要能在漆黑的夜晚看見陰影中的捕食者,也能在烈日下看清物體。
因此,人眼對「暗部的微小亮度變化」極度敏感,而對「亮部的亮度變化」則相對遲鈍:
在全黑的房間裡點燃 1 根蠟燭,你會覺得世界瞬間被照亮;
但如果在正午的烈日下再點燃 1 根蠟燭,你甚至察覺不到光線有任何增加。
早期的陰極射線管(CRT)螢幕在物理結構上恰好具有非線性的電壓-亮度特性:輸入電壓與顯示亮度的關係是一條指數曲線(亮度 $\approx$ 電壓$^{2.2}$)。為了善用這項物理特徵,工程師決定將圖片儲存為 sRGB / Gamma 空間:把更多檔案位元(Bits)分配給人眼更在意的暗部細節,亮部則壓縮儲存。

如果直接在未校正的 Gamma 空間計算光照,所有光影衰減曲線都會被物理扭曲。因此,現代 Three.js 遵循嚴格的黃金流程:

在 Three.js 中,我們透過以下宣告確立了這個工作管線:
renderer.outputColorSpace = THREE.SRGBColorSpace;
這行指令確保了瀏覽器在最後一刻將顯卡算出的線性物理光線,平滑編碼回符合螢幕物理顯示的 sRGB 曲線。
搞懂了線性空間後,緊接著迎面而來的是第二道難關:顯卡運算的「光照強度」沒有上限,但電腦螢幕有!
在我們的地宮場景中:
背景月光強度約為 0.72;
壁燈發光體強度設定為 1.6;
隨身火把在近距離照射石磚時,能量疊加可以輕鬆飆到 4.0 ~ 8.0,而 Bloom 核心甚至突破 15.0。
這種真實記錄極端明暗對比的數值,稱為 高動態範圍(HDR)。
但是,常規的電腦螢幕(SDR 顯示器)每個像素能顯示的色彩極限就是 $[0.0, 1.0]$(或 0 ~ 255 整數):

如果沒有色調映射,顯卡只能粗暴地執行 clamp(color, 0.0, 1.0)。所有大於 1.0 的暖金火光、高溫火星與石磚高光,全部被一刀切成了 (1.0, 1.0, 1.0)——這就是為什麼你的火把周圍會出現一圈難看且平整的「純白圓形大餅」。
色調映射(Tone Mapping)就是數學上的「軟性壓縮器」。它的任務是:將 $0.0 \sim \infty$ 的無限光學數值,優雅、平滑地擠壓收斂進螢幕能顯示的 $[0.0, 1.0]$ 區間內。
在過去,遊戲常使用簡單的 Reinhard 算法($\frac{x}{x + 1}$),但它的壓縮過於線性,導致畫面缺乏對比度,呈現出一種灰濛濛的塑料感。在專案中,我們啟用了當今好萊塢電影與頂級遊戲引擎(如 Unreal Engine)的通用標準——ACES Filmic 色調映射:
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15; // 稍微拉升 15% 曝光,讓石磚細節與火光更通透
ACES(Academy Color Encoding System,美國電影藝術與科學學院色彩編碼系統)最初由好萊塢為了統一數位攝影機與傳統膠捲底片色彩而制定。
它最核心的特徵,是一條極度符合人眼感知的 「S 型響應曲線(S-Curve)」:
腳部(Toe - 豐富的暗部沉降):
在曲線最底端,微弱的數值被平滑壓暗。這讓地宮的陰影與深淵不會呈現灰白髒污,而是呈現沉穩深邃的冷藍紫夜色,烘托出古堡的幽閉感。
中間段(Midtones - 忠實的像素細節):
在人眼最敏感的中間亮度區,曲線保持斜率陡峭的線性過渡。2D 像素主角的皮膚、布料刻紋與石磚的裂縫,在此處獲得極高對比度,紋理顆粒感絲毫不受壓縮損害。
肩部(Shoulder - 夢幻的高光滾降 / Highlight Roll-off):
這是 ACES 的精髓所在!當火把中心強度高達 6.0 時,曲線不會陡然撞上 1.0 牆壁,而是漸進式地趨近 1.0。
這意味著:高溫的火光中心依然保留了豐富的暖金與琥珀色階,而不是直接燒成純白死色。光暈邊緣的衰減如同柯達傳統膠卷一般溫潤自然!
在 Day 18 中,當我們將原生渲染交接給 EffectComposer 後,曾在管線末端掛載了一個通道:
// 通道 4:色彩空間與色調映射輸出通道 (OutputPass)
const outputPass = new OutputPass();
composer.addPass(outputPass);
這個看似低調的 OutputPass,在 Three.js 現代架構(r154+)中扮演著決定畫面生死的中樞角色。
在傳統的渲染流程中,renderer.render(scene, camera) 會自動在著色完成時替你做完「ACES 色調映射」與「sRGB 編碼」。
但一旦啟用了後處理管線 EffectComposer:
多重 Pass 必須在線性 HDR 空間下運作:
我們在 Day 18 寫的 Bloom 輝光(門檻 0.98)與 9 採樣移軸景深,必須讀取超過 1.0 的真實 HDR 物理亮度才能正確擴散。如果過早將畫面壓扁成 $[0.0, 1.0]$,Bloom 就會完全失去威力。
Ping-Pong 緩衝區是 Half-Float 格式:
中間的所有離屏紋理(Render Target)都保留了高精度物理光學數據。
沒有人負責收尾:
如果不掛載終端通道,顯卡會直接將這張「線性未編碼」的內部數據扔給螢幕,導致畫面灰白失真;如果掛載了舊式的 GammaCorrectionShader,則只轉換了色彩空間,卻遺漏了 ACES 色調映射,高光立刻死白。
Three.js 官方推出的 OutputPass 將這兩項終端轉換封裝在管線的最末端:
前置濾鏡無損流轉:
讓 RenderPass、UnrealBloomPass、TiltShiftPass 盡情在 HDR 空間揮灑。
一錘定音:
在最後輸出至 HTML Canvas 的那一個瞬間,一次性執行 ACESFilmicToneMapping 的高光收斂,緊接著套用 sRGB 編碼輸出。
這樣做不僅保證了後處理光學濾鏡的真實度,更徹底消滅了色調斷階(Banding)與重複轉換的效能浪費!
【本日結語】
今天我們穿透了 WebGL 最深奧的色彩迷宮:
線性工作流(Linear Workflow):破解 CRT 時代留下的 Gamma 騙局,確保光照在物理線性空間相加,在 sRGB 空間呈現。
HDR 與色調映射:理解為什麼大於 1.0 的光線不能直接被 Clamp 截斷,而是需要平滑壓縮。
ACES Filmic 的 S 曲線美學:以暗部 Toe 沉降黑度、亮部 Shoulder 柔和高光滾降,讓 1850K 黑體火光呈現出底片般的醇厚暖金。
OutputPass 終端管線:在後處理多重 Ping-Pong 流水線中,嚴守 HDR 與色彩轉換的最後一道防線。
掌握了幾何、著色器、光影與色彩科學後,在下一篇 Day 26 中,我們將重返遊戲邏輯層——深入剖析 2D 動畫狀態機與固定時間步進(State Machine & Tick Engine)!解密待機、四向奔跑與火把換手背後的狀態解耦架構,並徹底根除畫面掉幀時動畫播放速度忽快忽慢的物理步進難題!