顏色的旅程

你選了一個顏色,它經過編碼、儲存、解碼,最後出現在畫布上——中間發生了什麼?

目錄

  1. 顏色是旅行者
  2. 35+1 色調色盤:每個顏色都有身份證
  3. RGB 三原色光:三盞燈的魔法
  4. HSB 色彩空間:另一種描述顏色的方式
  5. 色彩微調:hueShift / satShift / briShift
  6. 筆觸濃度:strokeLum 與 intensity
  7. 三種混合模式:Mix / Multiply / Darken
  8. 白色筆刷的秘密暗號 (歷史)
  9. Alpha 通道:像素的隱藏身份證 (歷史)
  10. 解碼還原:composite.frag 怎麼讀懂這些暗號 (歷史)
  11. 10b TypeMapBuffer — 現行身份識別系統 ✦
  12. 顏色在管線中的完整旅程
1

顏色是旅行者

想像你在文具店買了一支彩色筆,你拿起它在紙上畫一筆。看起來很簡單對吧?但在我們的程式裡,這支彩色筆的顏色其實要經過好幾個「車站」,才能最終出現在螢幕上——就像一封信要經過郵局分揀、傳送、派送,最後才到你家信箱。

在 InkField 裡,顏色的旅程大致是這樣的:

調色盤你選了什麼色?
encode.frag編碼 + 標記
feedback.frag墨水效果處理
finalBuffer儲存在記憶體
composite.frag解碼 + 合成
螢幕你看到的畫面

這篇教學會帶你走過每一個車站,看看顏色到底經歷了什麼。

2

35+1 色調色盤:每個顏色都有身份證

想像全班 36 個同學,每個人都有一個學號。老師不用記名字,只要喊學號就能找到人。我們的調色盤也一樣——35 種預設顏色加 1 個自訂色,每種都有一個 ID 編號。

調色盤的顏色定義在 js/colors.js 裡,每個顏色都有:

全部 36 色一覽

0
1
2深灰
3中灰
4淺灰
5
6
7咖啡
8墨綠
9深藍
10
11淺綠
12
13藍灰
14赭石
15橄欖
16粉紅
17酒紅
18金橙
19灰褐
20鼠尾草
21磚紅
22銀灰
23米色
24青灰
25駝色
26卡其
27霧玫瑰
28淡紫灰
29畫布色
30
31
32
33自訂
34珊瑚
35薄荷

幾個特殊學號

ID名稱特殊之處
0黑色預設色,使用完全不同的混合邏輯(去飽和度)
1白色走完全不同的「白色筆刷」編碼路線
29畫布色等於背景色,用來「擦除」
33自訂色不在調色盤裡,由使用者用滑桿自選

當你在面板上點一個色票,程式會設定 brushColorMode = id,告訴 Shader「嘿,現在要用學號 X 的顏色畫畫」。Shader 收到學號後,用一長串 if/else 找到對應的 RGB 值。

3

RGB 三原色光:三盞燈的魔法

想像一間完全漆黑的房間裡有三盞燈——紅色綠色藍色。每盞燈都有一個旋鈕,可以調亮度(0~255)。三盞燈同時打開、調到最亮,你看到的就是白色。全部關掉就是黑色。這就是 RGB!
R
+
G
+
B
=

在 Shader 裡,RGB 值是 0.0~1.0

日常用的 0~255 其實是把 0.0~1.0 切成 256 格。所以:

  • 黑色 #1A1A1A = vec3(26/255, 26/255, 26/255)vec3(0.102)
  • 純紅 #FF0000 = vec3(1.0, 0.0, 0.0)
  • 純白 #FFFFFF = vec3(1.0, 1.0, 1.0)

Shader 裡有一個很常用的數字叫亮度(Luminance),它把 RGB 三個值加權平均:

float lum = dot(color, vec3(0.299, 0.587, 0.114)); // 意思是:亮度 = R×0.299 + G×0.587 + B×0.114 // 為什麼不是平均?因為人眼對綠色最敏感!
為什麼綠色權重最大?因為人眼的感光細胞裡,對綠光敏感的最多。就像一間教室裡綠色小組的人最多,投票的時候綠色票數自然最高。
4

HSB 色彩空間:另一種描述顏色的方式

如果 RGB 是「用三盞燈混色」,HSB 就像用更直覺的方式描述顏色:
H(色相)= 你在彩虹上挑哪個位置(0~360°)
S(飽和度)= 顏色有多鮮豔?0 = 灰灰的,1 = 超鮮豔
B(亮度)= 顏色有多亮?0 = 全黑,1 = 最亮

為什麼需要 HSB?

因為「把顏色調亮一點」用 RGB 很難做——你要同時改三個值。但用 HSB 只要調一個旋鈕(B)就好。程式裡需要微調顏色的時候,會先把 RGB 轉換成 HSB,調完再轉回來:

vec3 hsb = rgb2hsb(brushColor); // RGB → HSB hsb.x = mod(hsb.x + hueShift, 1.0); // 調色相 hsb.y = clamp(hsb.y + satShift, 0.0, 1.0); // 調飽和度 hsb.z = clamp(hsb.z + maxBriShift, 0.0, 0.95); // 調亮度 adjustedColor = hsb2rgb(hsb); // HSB → RGB

HSB 轉換的小陷阱

RGB ↔ HSB 的轉換不是百分之百精確的。特別是當飽和度很低的時候(幾乎是灰色),色相(H)值會變得不穩定——就像問一片灰色牆壁「你是什麼顏色」,答案可能每次都不同。

這就是為什麼 encode.frag 裡有一段保護:當飽和度低於 0.05 時,直接設成 0,避免產生奇怪的偏色。

5

色彩微調:hueShift / satShift / briShift

調色盤上的 36 個顏色是「出廠預設值」。但你可以透過三個旋鈕做微調——就像買了一件衣服,但你可以稍微改褲腳長度、收腰、換扣子,讓它更適合你。

三個調整旋鈕

旋鈕對應 HSB效果
hueShiftH(色相)整個色環上旋轉,紅變橙、橙變黃…
satShiftS(飽和度)+鮮豔 / −灰暗
briShiftB(亮度)+更亮 / −更暗(上限 0.95,不會純白)

有些顏色不讓你調

灰階色(id 2、3、4)的 changeShift 被設成 0,三個旋鈕完全不生效。為什麼?因為灰色沒有色相和飽和度可以調,硬調反而會出現奇怪的偏色。

另外,id 29(畫布色)也走特殊路線——直接強制設成 HSB(0, 0, 1)(純白),跳過所有微調。因為它的功能是「擦除」,不需要偏色。

亮色的亮度上限

如果顏色本身已經很亮(B > 0.6),亮度微調的上限會被壓縮到 0.95 − 目前亮度。這是為了避免亮色調到幾乎全白,跟白色筆刷搞混。

float maxBriShift = (hsb.z > 0.6) ? (0.95 - hsb.z) : briShift; // 如果已經很亮,只能再亮一點點
6

筆觸濃度:strokeLum 與 intensity

你用毛筆沾墨,然後在紙上畫。墨汁多的地方顏色深,墨汁少的邊緣顏色淡。在程式裡,筆觸的「墨量」就是 intensity(強度)。

encode.frag 先讀取筆觸圖層(strokeTex),算出每個點的亮度:

float strokeLum = dot(stroke, vec3(0.299, 0.587, 0.114)); float baseIntensity = clamp(1.0 - strokeLum, 0.0, 1.0); // strokeLum 越暗 → baseIntensity 越高 → 墨量越多

「快速出局」門檻

如果 strokeLum > 0.9(幾乎純白 = 沒畫到),整個像素直接跳過,保留原本的顏色。這就像掃過紙面但沒碰到的部分,不用浪費時間處理。

接下來,baseIntensity 會經過一條「曲線」變成最終的 intensity,不同墨水效果模式用不同曲線:

各模式的 intensity 曲線

useSharpen公式效果
0(墨汁擴散)1.02 × clamp(0.15 + 0.85 × x0.6)起始就有 15% 底色,擴散感強
1, 2, 3x0.7中等提亮,適合紋理效果
4, 5(水彩)黑筆 x1.5 × 0.98
彩筆 x0.7
黑筆壓暗避免水彩白點過亮

x = baseIntensity。次方 < 1 = 提亮(淡墨區域變明顯),次方 > 1 = 壓暗(只有濃墨才明顯)。

7

三種混合模式:Mix / Multiply / Darken

當彩色筆畫在已有顏色的地方時,新舊顏色要怎麼「合在一起」?這裡有三種模式可以選(由 keyBlendMode 控制):

Mode 0 — Mix(線性混合)

就像在調色盤上把兩團顏料混在一起。比例由 intensity 決定——墨量越多,新色比例越高。
result = mix(oldColor, newColor, intensity); // mix(A, B, t) = A × (1-t) + B × t // intensity = 0 → 完全保留舊色 // intensity = 1 → 完全變新色

Mode 1 — Multiply(相乘)

想像把兩片彩色玻璃紙疊在一起看。光線要通過兩層過濾,所以結果一定比原本更暗。紅色玻璃 × 藍色玻璃 = 很暗的紫色。
vec3 colorFilter = mix(vec3(1.0), adjustedColor, intensity); result = oldColor * colorFilter; // intensity 低 → filter 接近白色(1.0) → 幾乎不變 // intensity 高 → filter 接近筆刷色 → 強烈的色彩過濾

Mode 2 — Darken(取暗)

兩個顏色比身高,每個通道(R、G、B)都選矮的那個。結果只會更暗或一樣暗,不會更亮。
vec3 darkenedColor = min(oldColor, adjustedColor); result = mix(oldColor, darkenedColor, intensity); // min 逐通道取較小值
Mix
舊 + 新 → 漸變混合
Multiply
舊 × 新 → 一定更暗
Darken
每通道取暗 → 保守變暗

黑色筆刷的特殊處理

brushColorIdx == 0(黑色)時,不走上面三種模式,而是走專屬邏輯:

  • 灰色底 + 白色底 → mix 混合後去飽和度(避免染色)
  • 灰色底 + 非白 → min 取暗
  • 彩色底 → min 取暗後去飽和度(只留明暗,不留色彩)

「去飽和度」就是把 HSB 的 S 設成 0——把彩色變成灰色。因為黑色墨水蓋上去,不應該讓底色的彩度透出來。

⚠ 歷史架構(Ch8–Ch10)

以下 Ch8–Ch10 描述的是 2026-03-03 之前使用的 G = R × 0.5 暗號系統。該系統已被 TypeMapBuffer 獨立身份緩衝區(Ch10b)取代。

保留這些章節是因為它們記錄了一段重要的架構演化過程——從「在顏色通道內藏暗號」到「獨立身份緩衝區」的轉變,以及這個過程中產生的「紫色幽靈」Bug。

8

白色筆刷的秘密暗號 (歷史架構)

這是整個顏色系統裡最精彩的橋段!白色筆刷不是真的把像素塗白——它要在深色墨水上面「疊加光」。但問題來了:所有顏色儲存在同一張圖裡,程式怎麼知道「這個白色像素是白色筆刷畫的,還是本來就很亮的彩色」?答案是——藏暗號

白色筆刷的編碼公式

brushCategory > 0.5(白色筆刷模式)時,encode.frag 不走正常混色,而是:

// 白色筆刷的秘密編碼 float whiteLuminance = mix(1.0, 0.5, enhancedIntensity); result.r = whiteLuminance; // R = 亮度(0.5~1.0) result.g = result.r * 0.5; // G = R 的一半(這就是暗號!) result.b = whiteMaxOpacity; // B = 最大不透明度

看到了嗎?G = R × 0.5 就是白色筆刷的秘密暗號。正常的顏色不會有這種巧合(綠色通道剛好等於紅色通道的一半),所以之後 composite.frag 看到這個比例,就知道「啊,這是白色筆刷!」

三個通道各自的任務

通道儲存的資訊範圍
R(紅)筆觸亮度(越亮=越淡)0.5(最濃)~ 1.0(沒畫)
G(綠)暗號 = R × 0.5自動計算
B(藍)最大不透明度由 UI 滑桿決定
就像間諜在信件裡藏了一個暗號:如果你看到信的第二段字數剛好是第一段的一半,那這封信就是間諜寫的。只要收信人知道這個規則,就能辨認出來。

暗號的風險:紫色幽靈 Bug

這個暗號系統有一個弱點——如果 GPU 在縮放圖像時做了「雙線性插值」(把旁邊的像素混在一起),暗號比例會被打破。這時 G ≠ R×0.5,composite.frag 認不出白色筆刷,就會把這個像素當作一般彩色顯示。而 R 值高、G 值是 R 的一半附近、B 值也不低——這組數值看起來就像紫色!

這就是著名的「紫色幽靈」Bug,詳見除錯偵探故事集

9

Alpha 通道:像素的隱藏身份證 (歷史架構)

每個像素除了紅綠藍三個顏色通道,還有一個「Alpha」通道——通常用來表示透明度。但在 InkField 裡,Alpha 被拿來當「身份證」,記錄這個像素是用什麼類型的筆刷畫的。

brushTypeMarker 的三種身份

A = 0.99
黑色筆刷
0.995~1.0
彩色筆刷
Alpha 值身份什麼時候?
0.99 黑色筆刷 / 白色(id=1) brushColorIdx == 0brushColorIdx == 1
0.995 ~ 1.0 彩色筆刷 brushColorIdx > 1,數值 = 0.995 + intensity × 0.005
// encode.frag 最後一步:貼上身份證 float brushTypeMarker; if(brushColorIdx == 0.0) { brushTypeMarker = 0.99; // 黑色 } else if(brushColorIdx > 1.0) { brushTypeMarker = 0.995 + intensity * 0.005; // 彩色 brushTypeMarker = clamp(brushTypeMarker, 0.995, 1.0); } else { brushTypeMarker = 0.99; // 白色(id=1) } gl_FragColor = vec4(result, brushTypeMarker);
想像你在學校裡,每個人的名牌除了名字,角落還印了一個小小的數字:0.99 表示「我是黑白組」,0.995~1.0 表示「我是彩色組,而且這個數字越大表示我的顏色越濃」。

彩色筆刷的 intensity 也藏在 Alpha 裡

注意到了嗎?彩色筆刷的 Alpha 不是固定值,而是 0.995 + intensity × 0.005。這意味著:

  • intensity = 0(沒畫到)→ Alpha = 0.995
  • intensity = 1(最濃)→ Alpha = 1.0

稍後 composite.frag 就能從 Alpha 值反推出 intensity,用來決定合成的透明度!

10

解碼還原:composite.frag 怎麼讀懂這些暗號 (歷史架構)

encode.frag 是「寄信的人」,負責把顏色打包、貼暗號、蓋章。composite.frag 就是「收信的人」,它打開信封、辨認暗號、還原出真正的顏色,最終顯示在你的螢幕上。

composite.frag 的解碼流程就像一棵決策樹:

讀取像素的 RGB + Alpha
strokeLum > 0.995 且 colorDiff < 0.01?→ 幾乎純白 = 沒畫到,直接用背景
G ≈ R × 0.5 且 B > 0.4?→ 是白色筆刷暗號!走 Screen 混合
Alpha < 0.995?→ 黑色筆刷,走深色混合
Alpha ≥ 0.995?→ 彩色筆刷,走色彩濾鏡

白色筆刷的解碼

偵測到暗號(G ≈ R×0.5 且 B > 0.4)後,composite.frag 這樣還原:

float encodedLum = stroke.r; // 從 R 讀出亮度 float maxOpacity = stroke.b; // 從 B 讀出最大不透明度 float intensity = (1.0 - encodedLum) / 0.5; // 反推筆觸強度 // 用 Screen 混合——像在底色上打一盞白光 vec3 whiteColor = vec3(mix(0.3, 1.0, intensity)); vec3 screenResult = 1.0 - (1.0 - base) * (1.0 - whiteColor); result = mix(base, screenResult, maxOpacity);

Screen 混合的結果一定比原本更亮——就像在暗處打手電筒,只會更亮不會更暗。

彩色筆刷的解碼

從 Alpha 反推 intensity:

float encodedIntensity = (brushTypeMarker - 0.995) / 0.005; float alpha = clamp(encodedIntensity, 0.0, 1.0); // alpha = 0 → 沒畫到(0.995-0.995=0) // alpha = 1 → 最濃(1.0-0.995=0.005 → 0.005/0.005=1) // 用色彩濾鏡(Multiply 風格)合成 vec3 colorFilter = mix(vec3(1.0), safeStroke, alpha); result = base * colorFilter;

黑色筆刷的解碼

黑色筆刷比較直接——根據 strokeLum 計算透明度,然後混合:

float alpha; if(strokeLum > 0.5) { alpha = 0.95; // 淡墨區保持一定透明度 } else { alpha = 1.0 - smoothstep(0.0, 0.95, strokeLum); } result = mix(base, safeStroke, alpha);

安全防護:desaturate 去紫

還記得白色筆刷的暗號可能被 GPU 插值破壞嗎?composite.frag 有一個安全網——如果發現像素看起來「有點像被破壞的白色編碼」(G ≈ R×0.5、B 比 G 高、R > 0.1),就把它強制轉成灰色,避免出現紫色幽靈:

float desatTag = abs(safeStroke.g - safeStroke.r * 0.5); if(desatTag < 0.08 && safeStroke.b > safeStroke.g + 0.05 && safeStroke.r > 0.1) { float gray = dot(safeStroke, vec3(0.299, 0.587, 0.114)); safeStroke = vec3(gray); // 強制去色 → 灰色 }

退化白色的拯救

還有一種情況:白色筆刷的像素經過 Flow 力場處理後,B 通道的值被 min() 壓低到 0.4 以下,暗號中的「B > 0.4」條件失敗了。這時候程式用「退化白色偵測」把它救回來:

bool degradedWhite = ( degradedTag < 0.03 && // G ≈ R*0.5 暗號還在 stroke.r > 0.1 && stroke.r < 0.98 && stroke.b > stroke.g && // B 比 G 高(殘留特徵) !isRedFalsePositive // 排除紅色誤判 ); if(degradedWhite) { result = base; // 直接用背景色,假裝沒看到 }
10b

TypeMapBuffer — 現行身份識別系統 (2026-03-03 起)

為什麼需要新系統?

舊的 G = R × 0.5 暗號系統有致命弱點:它把身份資訊藏在顏色通道裡。只要 GPU 對像素做插值(縮放、Flow 力場位移),暗號比例就會被破壞,產生「紫色幽靈」等一系列 Bug(Cases 1–7,見 bug-stories.html)。

解法很簡單——把身份資訊從顏色通道裡搬出來,存進一張獨立的 Buffer。

舊系統就像在學生的衣服上縫一個暗號標籤——洗衣機(GPU 插值)一攪就壞了。新系統是另外發一張 獨立的學生證,衣服怎麼洗都不影響身份辨認。

Shader typeMapBuffer 的兩個通道

通道儲存的資訊數值
R(紅) 筆刷類型 0.0 = 背景(沒畫過)
0.5 = 彩色或黑色筆刷
1.0 = 白色筆刷
G(綠) 白色筆刷最大不透明度 由 UI 滑桿決定(僅白色筆刷使用)

三階段實施

階段內容影響的 Shader
Phase 1 建立 typeMapBuffer,每筆繪製時由 typeMapEncode.frag 寫入身份 typeMapEncode.frag(新增)、composite.frag(改讀 typeMapTex)
Phase 2 Flow 力場位移時,typeMapBuffer 同步跟隨顏色移動 flow.frag(新增 isTypeMapMode 雙 pass)
Phase 3 移除所有 G=R×0.5 殘留代碼 encode.frag、flow.frag、feedback.frag、composite.frag

composite.frag 新的解碼流程

不再需要「偵測暗號」——直接讀 typeMapTex:

vec4 typeInfo = texture2D(typeMapTex, uv); float brushType = typeInfo.r; float whiteMaxOpacity = typeInfo.g; if(brushType > 0.75) { // 白色筆刷 → Screen 混合(疊光) } else if(brushType > 0.25) { // 彩色或黑色筆刷 → 色彩濾鏡 / 深色混合 } else { // 背景 → 直接用紙張紋理 }

不再依賴通道比例,不再怕 GPU 插值破壞——從架構層面消除了紫色幽靈問題。

Flow 力場的同步

Flow 效果會把像素位移到新位置。如果只移動顏色、不移動身份,composite.frag 就會把白色筆刷的像素當成彩色來解碼。

解法:flow.frag 執行 兩次 pass——先用 isTypeMapMode = 1 移動 typeMapBuffer,再用 isTypeMapMode = 0 移動顏色。兩次使用相同的噪聲偏移量,確保身份跟著顏色走。

11

顏色在管線中的完整旅程

讓我們把所有車站串起來,看看一個彩色像素從頭到尾的完整旅程:

你點了調色盤上的「紫色」(id=10) brushColorMode = 10 → RGB(140, 106, 172)
encode.frag:編碼 1. 查表得到紫色 RGB
2. HSB 微調(hueShift, satShift, briShift)
3. strokeLum → intensity(曲線處理)
4. mix(淺灰, 調整色, intensity) → 帶有濃淡的紫色
5. Alpha = 0.995 + intensity×0.005 → 身份證
feedback.frag:墨水效果 擴散、紋理、暈染… 在像素之間互相影響
finalBuffer:儲存 帶有暗號和身份證的像素安靜地躺在記憶體裡
composite.frag:解碼 1. 讀 Alpha → 0.997 → 是彩色筆刷!
2. 反推 intensity = (0.997-0.995)/0.005 = 0.4
3. 用色彩濾鏡 mix(白, 紫色, 0.4) 處理
4. base × colorFilter → 最終顏色
螢幕上出現一筆漂亮的紫色! 有濃有淡、有暈染效果的毛筆紫

白色筆刷的不同旅程

白色筆刷走的是完全不同的路線:

你選了白色筆刷 brushCategory = 1
encode.frag:藏暗號 R = 亮度、G = R×0.5、B = maxOpacity
composite.frag:辨認暗號 G ≈ R×0.5?→ Screen 混合 → 在底色上疊白光

總結對照表

筆刷類型encode 怎麼標記composite 怎麼解碼混合方式
黑色(id=0) 去飽和度 + Alpha=0.99 Alpha < 0.995 → 黑色 mix / min + desaturate
白色(id=1) G=R×0.5, B=opacity 偵測 G≈R×0.5 暗號 Screen(疊光)
彩色(id>1) HSB 微調 + Alpha=0.995~1.0 Alpha ≥ 0.995 → 反推 intensity Color filter(Multiply 風格)
11

高彩度底色的混合難題 (2026-03-15)

想像把一片紅色玻璃紙疊在藍色玻璃紙上——光線要同時通過紅和藍的過濾,但紅色只讓紅光通過、藍色只讓藍光通過,結果幾乎沒有光能穿透。這就是 Multiply 混合在互補色上的數學宿命。

問題:互補色 Multiply 必然變黑

Ch7 介紹的 Multiply 模式 base × stroke 在低彩度底色上效果很好(像真實墨水疊加),但在高彩度底色遇到互補色時會崩潰:

// 藍色底 × 紅色筆刷 base = vec3(0.0, 0.0, 1.0); // 純藍 stroke = vec3(1.0, 0.0, 0.0); // 純紅 result = base * stroke; = vec3(0.0, 0.0, 0.0); // 黑色!

RGB 三通道沒有共同的高值,相乘後全部趨近零。這不是 bug,是 Multiply 的數學本質。

解法:底色彩度驅動的混合漸變

核心思路:低彩度底色保持 Multiply(墨色疊加感),高彩度底色漸變為 Normal Mix(筆刷色彩可見)。

float baseSat = rgb2hsb(base).y; float satBlend = smoothstep(0.3, 0.8, baseSat); vec3 mulResult = base * colorFilter; // Multiply(原本的方式) vec3 mixResult = mix(base, safeStroke, alpha); // Normal Mix result = mix(mulResult, mixResult, satBlend); // baseSat < 0.3 → 100% Multiply(紙紋保留) // baseSat > 0.8 → 100% Normal Mix(色彩可見) // 中間 → smoothstep S 曲線漸變

進階:筆刷亮度歸一化(realtime.frag)

即時繪畫(realtime.frag)中需要額外處理筆刷邊緣。邊緣的墨量少、亮度高,需要保持 Multiply 才能自然消融。但亮色筆刷(米色、粉紅)本身就亮,不能把「亮」都當「邊緣」處理。

// 用筆刷自身亮度做歸一化 float brushLum = dot(adjustedColor, vec3(0.299, 0.587, 0.114)); float strokeDensity = clamp( (1.0 - addLum) / max(1.0 - brushLum, 0.01), 0.0, 1.0 ); // 紅色筆刷(brushLum≈0.3):中心 density=1, 邊緣 density≈0.14 // 米色筆刷(brushLum≈0.8):中心 density=1, 邊緣 density≈0.25

歸一化讓 strokeDensity 判斷的是「相對於筆刷原色的濃淡」,而非「絕對亮度」。

兩個 Shader 的策略差異

Shader有無筆刷色資訊策略
realtime.frag 有(adjustedColor satBlend × strokeDensity(brushLum 歸一化)
composite.frag 直接用 satBlend(不乘 strokeDensity)

← 上一篇:墨水效果全圖鑑   |   下一篇:錄影帶的秘密 →

InkField 教學系列 — 顏色的旅程