後處理特效

筆觸畫完之後,力場、擴散、漣漪接手——讓靜止的墨跡繼續變化

目錄

  1. 畫完之後的「調味料」
  2. Flow 力場:看不見的風在吹你的畫
  3. Flow 的八種造型模式
  4. FBM 扭曲:隔著水面看你的畫
  5. Resonance Scatter:往池塘丟 16 顆石頭
  6. Cellular / Voronoi:顯微鏡下的細胞
  7. 金屬光澤:讓墨跡變成液態金屬
  8. White Dot & Film Grain:紙上的小瑕疵
  9. 效果組合的藝術
  10. Polygon Mask:限制繪畫區域
1

畫完之後的「調味料」

想像你做完一道菜——基本的食材和烹飪已經完成了。但接下來你可以加鹽巴、灑胡椒、擠檸檬汁、淋醬油…這些「調味料」不會改變食材本身,但會讓風味截然不同。InkField 的後處理特效就是畫完之後的調味料。

所有特效都作用在「已經畫好的圖」上,不會改變你的筆觸——它們只是改變這些筆觸被「看到」的方式。主要的特效來自兩個 Shader:

🌊

flow.frag

Simplex 噪波位移、多種造型模式、邊緣飛白

🌀

distort.frag

FBM 扭曲、漣漪散射、細胞紋路、白點、底片顆粒

metallic.frag

金屬反射、Fresnel、鑽石色散、黑金紋理

📖 更多閱讀:inkField 的變形效果源自前作春分|Equinox——為了融合兩種季節而拉扯 buffer 形成另一種狀態,是春分的核心技術原理

2

Flow 力場:看不見的風在吹你的畫

想像你用墨水在紙上畫了一幅畫,然後有一陣看不見的風從各個方向吹過來——墨跡被風推著移動、扭曲、暈開。不同位置的風向不同、風速也不同。這就是 Flow 力場的原理。

flow.frag 的核心運作原理是「從別的位置取樣」

// 基礎噪波位移:用 Simplex Noise 算出每個點該往哪偏移 crd2 += simpleN2(coord * 0.001) * px * blendVol; crd2 += simpleN2(coord * 0.005) * px * blendVol / 2.0; // 然後從偏移後的位置讀取像素 vec4 sample1 = texture2D(tex0, crd2); // 取原位置和偏移位置的較暗值 vec4 finalColor = min(sample0, sample1);

為什麼用 min()?

min(原位置顏色, 偏移位置顏色) 的效果是:如果偏移後的位置更暗(有墨),就把那個墨「拉」過來。這樣墨跡會往四周「蔓延」,但亮的地方不會覆蓋暗的地方——跟真實墨水在紙上暈開的效果很像。

Simplex Noise 是什麼?

它是一種數學函數,輸入一個座標,輸出一個「看起來很自然的隨機值」。跟純隨機(每個點都跳來跳去)不同,Simplex Noise 相鄰的點值很接近,遠一點的才差很多——就像真實的山脈地形:旁邊的海拔差不多,但翻過一座山就完全不同了。

在 flow.frag 裡,simpleN2() 輸出一個二維向量(x 偏移, y 偏移),就是每個點被「風」吹的方向。

3

Flow 的八種造型模式

除了基礎噪波位移,flow.frag 還有八種 blendType 造型模式:

blendType名稱效果
0基礎模式只有噪波位移,根據 gobalStyle 調整強度分區
2同心圓兩個隨機圓心產生徑向漣漪,用 mix() 取代基礎噪聲——圓心附近漣漪主導,外圍保留有機噪聲
3縱向多層 sin/cos/tan 組合,產生垂直方向的扭曲紋路
4橫向同上但方向翻轉,產生水平方向的扭曲紋路
5龜裂花紋雙層 Voronoi/cellular 噪聲驅動,cell 邊界處產生強位移形成龜裂/碎裂紋理,cell 內部保留噪聲
6馬賽克碎片畫面切成隨機大小的格子,每格獨立隨機偏移——像打碎的磁磚各自位移,格線邊界清晰
7漩渦兩個渦心從原始座標計算極座標旋轉,Gaussian 衰減——靠近渦心漩渦主導,遠離渦心過渡到噪聲
8細胞Voronoi 細胞紋路 + Simplex 擾動

邊緣飛白效果

所有 Flow 模式在筆觸邊緣都有一個精巧的處理——邊緣半色調過渡(halftone dithering):

// 離邊緣越近,越可能「跳過」這個像素 float ditherNoise = hash(coord * noiseFreq); edgeFade = step(ditherNoise, distRatio); // distRatio = 0(邊緣)→ 幾乎都跳過 → 飛白散落 // distRatio = 1(中心)→ 幾乎都保留 → 飽滿墨跡

這就像真實墨水在紙面邊緣自然散開的飛白效果——不是整齊的漸變,而是像墨點飛濺一樣的隨機點狀。

白色筆刷保護(TypeMapBuffer)

白色筆刷的身份現在由獨立的 typeMapBuffer 管理(舊的 G=R×0.5 暗號系統已移除)。Flow 對 typeMap 做獨立的同步 pass(isTypeMapMode=1),用 nearest-neighbor 取樣避免插值模糊身份:

// typeMap pass:nearest-neighbor 取樣,保守傳播身份 vec2 nearestCrd = (floor(crd * rect.zw) + 0.5) / rect.zw; vec2 nearestCrd2 = (floor(crd2 * rect.zw) + 0.5) / rect.zw; vec4 type0 = texture2D(tex0, nearestCrd); vec4 type1 = texture2D(tex0, nearestCrd2); // 匹配 color flow 的 min() 語義:深色墨水主導 gl_FragColor = (type1.r < type0.r) ? type1 : type0;

詳見除錯偵探故事集中 TypeMapBuffer 的完整架構說明。

4

FBM 扭曲:隔著水面看你的畫

FBM扭曲效果 — 隔著水面折射變形
想像把一幅畫放在水面下,你從水面上方看。因為水波的折射,畫面會產生美麗的扭曲——這就是 FBM 扭曲的效果。FBM 全名是 Fractal Brownian Motion(碎形布朗運動),聽起來很嚇人,但其實就是「多層噪波疊加」。

distort.frag 裡的 FBM 是這樣建構的:

float fbm4(vec2 p) { float f = 0.0; f += 0.5000 * noise(p); // 第 1 層:大山脈 p = m * p * 2.02; f += 0.2500 * noise(p); // 第 2 層:中型丘陵 p = m * p * 2.02; f += 0.1250 * noise(p); // 第 3 層:小坡 p = m * p * 2.02; f += 0.0625 * noise(p); // 第 4 層:碎石細節 return f; }
就像看一座山——遠看是一個大輪廓(第 1 層),近一點看有中型的丘陵(第 2 層),再近有小坡(第 3 層),最近看到碎石和草(第 4 層)。四層疊在一起,就產生了自然界那種「越看越有細節」的碎形質感。

func() 函數:FBM 的 FBM

distort.frag 裡最核心的 func() 函數更進一步——它用 FBM 的結果去偏移另一個 FBM,再用那個結果偏移第三個 FBM。就像用山的形狀去扭曲另一座山,再用結果扭曲第三座山。最終產生的形狀極其自然、充滿有機感。

vec2 o = fbm4_2(0.9 * q); // 第一層 FBM → 得到偏移向量 vec2 n = fbm6_2(3.0 * o); // 用偏移結果去採樣第二層 FBM float f = fbm4(1.8 * q + 6.0 * n); // 再用它偏移第三層

Distort 怎麼用 FBM 的?

開啟 distort 後,程式用 FBM 值作為「遮罩」(fbmMask),控制力場位移的強度:

  • fbmMask 高的區域 → 位移強 → 扭曲明顯
  • fbmMask 低的區域 → 位移弱 → 幾乎不變

另外還會根據力場方向做色相偏移——暗色區域的顏色會隨力場方向微微變化,增加有機感。

5

Resonance Scatter:往池塘丟 16 顆石頭

漣漪散射效果 — 池塘石子波紋疊加
想像一個平靜的池塘,你同時往裡面丟了 16 顆石頭。每顆石頭都會產生一圈圈的漣漪,漣漪彼此交疊——有的地方波峰相遇變得更高(建設性干涉),有的地方波峰遇到波谷互相抵消(破壞性干涉)。這就是 Resonance Scatter。

16 個波源

程式在畫面上均勻放置 16 個點(4×4 格子),每個點的位置會根據 forceMap(力場)略微偏移。然後計算每個像素到這 16 個點的距離,用正弦波模擬漣漪:

for(int i = 0; i < 16; i++) { vec2 pt = getResonancePoint(i); // 波源位置 float weight = getPointWeight(i); // 權重(力場越強越重要) float L = length(pos - pt); // 到波源的距離 C += sin(TAU * f * (t - L/v) / scale) * weight; // ↑ 經典波動方程:sin(頻率 × (時間 - 距離/速度)) }

漣漪怎麼變成位移?

最終的位移是兩部分的混合:

  • 振盪位移:波的值直接當位移(像素跟著波上下左右晃)
  • 梯度位移:波的坡度方向當位移(像素順著波的斜面滑動)

rsGradientMix 控制兩者的比例——振盪為主像「水面波紋」,梯度為主像「光線折射」。

6

Cellular / Voronoi:顯微鏡下的細胞

細胞紋路效果 — Voronoi地盤分割
想像一塊草地上散落了很多種子。每顆種子都在搶地盤——離哪顆種子最近的區域就屬於那顆種子。最後形成的地盤邊界就像細胞壁,每塊地盤就是一個「細胞」。這就是 Voronoi 圖。

cellular2x2() 函數

distort.frag 裡的 cellular2x2() 計算兩個最近距離 F.x 和 F.y(最近和次近的「種子」距離):

vec2 F = cellular2x2(cellUV); float facets = F.y - F.x; // 次近距離 - 最近距離 float dots = smoothstep(0.05, 0.3, F.x); float mask = step(0.4, facets) * dots; // facets 大 → 離邊界遠(細胞內部) // facets 小 → 在邊界上(細胞壁)

然後用這個 mask 控制位移強度——邊界處位移大、內部位移小,產生「細胞壁更暗」的效果。

時間動畫

Cellular 效果會緩慢漂移(速度 ×0.7),讓細胞紋路看起來像在「流動」。邊緣會用 smoothstep 做 10% 的過渡,避免畫面邊緣突然截斷。

7

金屬光澤:讓墨跡變成液態金屬

金色金屬效果
金色 Gold
銀色金屬效果
銀色 Silver
想像你畫的墨跡突然變成了液態水銀——表面有鏡面反射、邊緣會更亮(像金屬器皿的光暈),甚至像鑽石一樣折射出彩虹。這就是 metallic.frag 做的事。

金屬光澤只作用在「有墨跡的地方」——用 bugsMask(筆觸遮罩)控制哪裡有效果、哪裡沒有。

反射與法線貼圖

程式從遮罩的濃淡變化計算「表面法線」——墨跡邊緣濃度變化大的地方,表面就像「斜坡」,光線會往旁邊反射:

// 從遮罩梯度計算法線 float dx = (maskRight - maskCenter) * resolution.x * 0.5; float dy = (maskTop - maskCenter) * resolution.y * 0.5; vec3 normal = normalize(vec3(dx, dy, sqrt(1.0 - dx*dx - dy*dy)));

法線就像表面每個點的「朝向」——平面朝上,斜坡朝旁邊。有了法線,就能算出光線打在表面上會往哪裡反射。

Fresnel 效果:邊緣更亮

站在湖邊,低頭看腳下的水——你能看到水底。但往遠處看,水面變成了鏡子,反射天空。同一片水,不同角度看反射率不同。這就是 Fresnel 效果。

Metallic shader 用 Schlick 近似公式計算 Fresnel:

float fresnel = 1.0 - pow(dot(normal, -viewDir), 1.0); // 法線朝向你 → dot 大 → fresnel 小 → 不太亮 // 法線朝旁邊 → dot 小 → fresnel 大 → 邊緣更亮

六種金屬色調

色調RGB特殊處理
Gold(0.88, 0.72, 0.52)暖色反射
Silver(0.75, 0.75, 0.75)灰階檢測 → 中性反射
Copper(0.72, 0.50, 0.35)紅銅色調
Rose(0.88, 0.65, 0.70)玫瑰金
Black Gold(0.15, 0.12, 0.08)特殊黑金紋理函數 + 鏽蝕細節
Diamond(0.95, 0.95, 1.0)色散折射!RGB 三色不同折射率

鑽石色散

鑽石模式是最華麗的——它模擬了真實鑽石的「色散」(dispersion)效果。鑽石對不同顏色的光有不同的折射率:

const float diamondIOR_r = 2.408; // 紅光折射率 const float diamondIOR_g = 2.424; // 綠光折射率 const float diamondIOR_b = 2.432; // 藍光折射率 // 三色分別折射,各自從不同位置取樣 // → 結果:邊緣出現彩虹光暈

因為紅光彎得最少、藍光彎得最多,三色在邊緣處分開了——就像真正鑽石的「火彩」。

8

White Dot & Film Grain:紙上的小瑕疵

白點效果 — 紙張針孔與斑點
White Dot 白點
底片顆粒效果 — 復古膠卷質感
Film Grain 底片顆粒
真實的紙張不是完美的——上面會有小針孔、斑點、刮痕。老底片上有顆粒感。這些「瑕疵」反而讓作品看起來更有溫度、更像手工藝品。

三種尺寸的白點

層次大小描述
Layer 1~5px小針孔(punctures)——像紙面上被針戳的小洞
Layer 210~30px中型斑點(spots)——隨機半徑的圓形白斑
Layer 340~100px大型刮痕(gouges)——橢圓形、有方向性的痕跡

每一層都用 hash 噪波控制,而且有「叢集」效果——白點傾向聚在一起出現,不會均勻散佈。密度由 whiteDotDensity 統一控制。

Film Grain:底片顆粒

模擬真實底片的顆粒感,有三個特色:

  • 暗處更多顆粒:亮度越低的區域,顆粒越明顯(像真實底片)
  • 筆觸處更多顆粒:力場強的地方(有筆觸活動)顆粒更密
  • 多頻率混合:細顆粒 60% + 中團粒 30% + 粗斑塊 10%
float gFine = grain(coord); // 細顆粒 float gMedium = grain(floor(coord * 0.5) * 2.0); // 中團粒 float gCoarse = hash(floor(coord * 0.15)); // 粗斑塊 float g = gFine * 0.6 + gMedium * 0.3 + gCoarse * 0.1;
9

效果組合的藝術

這些效果可以同時開啟、疊加使用。以下是整個特效管線的處理順序:

flow.frag(每筆觸後執行) Simplex 位移 + blendType 造型 + 邊緣飛白 + 白色筆刷保護
distort.frag(每幀即時渲染) FBM 扭曲 → Resonance Scatter → Cellular → White Dots → Film Grain
metallic.frag(每幀即時渲染) 反射 + Fresnel + 色散 + 金屬紋理

建議組合

風格建議組合
古典水墨Flow (基礎模式) + 無 Distort + 少量 White Dot + Film Grain
抽象藝術Flow (漩渦) + FBM Distort + Resonance Scatter
生物紋理Flow (細胞) + Cellular + Film Grain
金屬雕塑Flow (基礎) + Metallic (Gold/Diamond)
復古底片Flow (基礎) + 強 Film Grain + 大量 White Dot

小結

所有特效的共同原則是:不改變原始筆觸,只改變它被「看到」的方式。它們像一層層的濾鏡,可以疊加、可以單用、可以調整強度。這就是為什麼同樣的一筆,搭配不同的特效,可以看起來像水墨、像水彩、像金屬雕塑、像顯微鏡下的生物組織。

10

Polygon Mask:限制繪畫區域

有時候你只想在畫面的某個區域作畫,讓其他地方完全不受影響。Polygon Mask Tool 就是為此而生。

原理:Shader 層級的遮罩

Mask 的運作方式和 Photoshop 的遮罩類似,但直接在 GPU shader 層級實現:

  • polygonMaskBuffer:獨立的 WebGL framebuffer,白色=可畫,黑色=不可畫
  • 四個 shader 都加入 useMask + maskTex uniform: encode.frag(筆觸編碼)、feedback.frag(墨水擴散)、realtime.frag(即時預覽)、typeMapEncode.frag(筆刷身份)
  • 每個 shader 的 main() 開頭都加上 early-return:if (mask < 0.5) { 回傳原始值; return; }
  • flow.fragcomposite.frag 不受 mask 控制 — flow 是全畫面變形,composite 是最終合成

兩種遮罩模式

Rect 矩形

拖拉畫矩形區域。按下開始、放開完成。綠色虛線預覽。

Polygon 多邊形

Toggle 按鈕開始加點(左鍵),再按一次封閉多邊形。至少 3 個頂點。

座標系統對齊

筆刷系統有 perspectiveOffset = -10。Mask 座標記錄時也套用 mouseX - 10, mouseY - 10,確保遮罩邊界和筆觸精確對齊。

WebGL framebuffer 的 Y 軸與螢幕相反(Y=0 在底部),所以 drawMaskRectdrawMaskPolygonheight - y 翻轉。

錄製與播放

Mask 狀態嵌入筆畫事件(strokeData.maskData),不使用獨立的 mask 事件。這避免了時間戳錯亂問題 — 獨立事件的時間在暫停期間會失準,但嵌入筆畫事件中就跟著筆畫走。

視覺化

遮罩用綠色虛線畫在 cursorBuffer(透過 grid.jsdrawMaskToGrid()),包括:已完成的遮罩邊框、拖拉中的矩形預覽、建構中的多邊形頂點預覽。統一用一個系統渲染,不需要額外的 HTML overlay canvas。

白色筆刷的陷阱

typeMapEncode.frag 也需要 mask 檢查!

如果只在 encode.frag(顏色)加 mask,但漏了 typeMapEncode.frag(身份),白色筆刷的 brushType = 1.0 會寫到 mask 外的 typeMap。composite.frag 看到 typeMap 標記為白色筆刷,就會用 Screen blend 處理那些像素 — 即使顏色沒變,混合模式的改變會造成淡白色滲出。

← 上一篇:墨水效果全圖鑑   |   下一篇:混色與力場 →

InkField 教學系列 — 用國中生也能懂的方式,認識數位水墨的秘密