ColorOverlayRenderer
基于 Metal 的 iOS 圖像顏色蒙版渲染器,支持一次性渲染和實(shí)時(shí)預(yù)覽兩種模式。核心能力是為任意 PNG/手繪圖像自動(dòng)生成閉合區(qū)域的彩色背景蒙版。
功能概覽
| 功能 | 方法 | 說明 |
|---|---|---|
| 一次性渲染 | applyOverlay(to:color:expandRadius:) |
輸入 UIImage + 顏色,輸出帶蒙版的 UIImage |
| 預(yù)處理 | prepareForRealtimeRendering(image:expandRadius:) |
緩存 CPU 泛洪 + GPU 膨脹結(jié)果,為實(shí)時(shí)預(yù)覽做準(zhǔn)備 |
| 實(shí)時(shí)預(yù)覽 | renderToView() |
僅執(zhí)行顏色疊加 Pass,渲染到 MTKView |
| 導(dǎo)出 | exportCurrentResult() |
將當(dāng)前預(yù)覽結(jié)果編碼為 UIImage |
| 清理 | cleanupRealtimeCache() |
釋放緩存紋理 |

)
架構(gòu)
┌─────────────────────────────────────────────────────────┐
│ ColorOverlayRenderer │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 一次性模式 (applyOverlay) │ │
│ │ UIImage → CPU泛洪 → Pass1 → Pass2 → UIImage │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 實(shí)時(shí)預(yù)覽模式 │ │
│ │ prepareForRealtimeRendering: │ │
│ │ UIImage → CPU泛洪 → Pass1 → 緩存紋理 │ │
│ │ │ │
│ │ renderToView (每次顏色變化): │ │
│ │ 緩存紋理 → Pass2 → Render Pass → MTKView │ │
│ │ │ │
│ │ exportCurrentResult: │ │
│ │ 緩存紋理 → Pass2 → 像素回讀 → UIImage │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Metal Pipeline 總覽
渲染器初始化時(shí)編譯 3 條 Pipeline:
| Pipeline | 類型 | Shader 函數(shù) | 用途 |
|---|---|---|---|
dilatePipelineState |
Compute | dilate_mask |
Pass 1: 將基礎(chǔ) Mask 按半徑做圓形膨脹 |
applyOverlayPipelineState |
Compute | apply_color_overlay |
Pass 2: 根據(jù)膨脹 Mask 和顏色生成最終蒙版圖像 |
renderPipelineState |
Render |
quad_vertex_main + quad_fragment_main
|
將 Compute 輸出紋理 Aspect-Fit 繪制到 MTKView |
功能一:一次性渲染 (applyOverlay)
完整的同步渲染流程,輸入 UIImage + 顏色,輸出帶蒙版的 UIImage。適用于不需要實(shí)時(shí)預(yù)覽的場景。
流程
UIImage
│
▼
┌─────────────────────────────┐
│ 1. 創(chuàng)建 Padded 畫布 │ 原圖四周各加 expandRadius 像素的透明邊距
│ (width + 2*radius) × │ 確保膨脹后的 Mask 不會(huì)被畫布邊界截?cái)?│ (height + 2*radius) │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 2. CPU 泛洪生成基礎(chǔ) Mask │ GraphicAlgorithm.generateSolidMask()
│ (詳見下方 "背景蒙版生成") │ 輸出: 單通道 [UInt8] (0=外部, 255=內(nèi)部)
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 3. 創(chuàng)建 Metal 紋理 │ inTexture (rgba8Unorm): 原始圖像像素
│ │ cpuMaskTexture (r8Unorm): CPU 生成的基礎(chǔ) Mask
│ │ dilatedMaskTexture (r8Unorm): 膨脹后的 Mask
│ │ outTexture (rgba8Unorm): 最終輸出
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 4. GPU Pass 1: dilate_mask │ Compute Shader
│ │ 輸入: cpuMaskTexture + expandRadius
│ │ 輸出: dilatedMaskTexture
│ │ 原理: 對每個(gè)像素搜索半徑內(nèi)是否存在 Mask 有效像素
│ │ 使用圓形內(nèi)核 (i2+j2 ≤ r2) 確保邊緣圓滑
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 5. GPU Pass 2: │ Compute Shader
│ apply_color_overlay │ 輸入: inTexture + dilatedMaskTexture + 顏色參數(shù)
│ │ 輸出: outTexture
│ │ 邏輯:
│ │ mask > 0.5 且 原始alpha > 0 → 保留原始像素(前景)
│ │ mask > 0.5 且 原始alpha = 0 → 填充指定顏色(背景)
│ │ mask ≤ 0.5 → 全透明(外部區(qū)域)
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ 6. 像素回讀 │ outTexture.getBytes() → CGContext → CGImage → UIImage
│ │ 保留原始 image.scale 和 image.imageOrientation
└─────────────────────────────┘
功能二:實(shí)時(shí)預(yù)覽模式
將渲染流程拆分為"預(yù)處理"和"實(shí)時(shí)渲染"兩個(gè)階段。預(yù)處理只執(zhí)行一次(CPU 泛洪 + Pass 1),后續(xù)顏色變化時(shí)僅重新執(zhí)行 Pass 2,實(shí)現(xiàn)毫秒級響應(yīng)。
階段 1: 預(yù)處理 (prepareForRealtimeRendering)
UIImage
│
▼
┌──────────────────────────────────┐
│ CPU 泛洪 + GPU Pass 1 (同上) │
│ │
│ 緩存結(jié)果: │
│ cachedInTexture ← inTexture │
│ cachedDilatedMaskTexture ← dilatedMaskTexture
│ cachedOutTexture ← outTexture │
│ cachedImageScale ← image.scale │
│ cachedImageOrientation │
│ isPrepared = true │
└──────────────────────────────────┘
階段 2: 實(shí)時(shí)渲染 (renderToView)
每次 overlayColor 變化時(shí)觸發(fā):
overlayColor didSet
│
▼
mtkView.setNeedsDisplay()
│
▼
MTKViewDelegate.draw(in:)
│
▼
renderToView()
│
├─── Compute Pass ──────────────────────────────────┐
│ apply_color_overlay: │
│ cachedInTexture + cachedDilatedMaskTexture │
│ → cachedOutTexture │
│ (僅執(zhí)行 Pass 2,跳過 CPU 泛洪和 Pass 1) │
└────────────────────────────────────────────────────┘
│
├─── Render Pass ───────────────────────────────────┐
│ quad_vertex_main + quad_fragment_main: │
│ 將 cachedOutTexture 以 Aspect-Fit 方式 │
│ 居中繪制到 drawable.texture │
│ │
│ NDC 坐標(biāo)計(jì)算: │
│ scaleFit = min(viewW/imgW, viewH/imgH, 1.0) │
│ ndcScaleX = (imgW * scaleFit) / viewW │
│ ndcScaleY = (imgH * scaleFit) / viewH │
│ 頂點(diǎn)范圍: [-ndcScaleX, -ndcScaleY] │
│ 到 [+ndcScaleX, +ndcScaleY] │
└─────────────────────────────────────────────────────┘
│
▼
commandBuffer.present(drawable) + commit()
階段 3: 導(dǎo)出 (exportCurrentResult)
用戶確認(rèn)保存時(shí)調(diào)用,同步執(zhí)行 Pass 2 并回讀像素:
exportCurrentResult()
│
▼
Compute Pass: apply_color_overlay → cachedOutTexture
│
▼
waitUntilCompleted() ← 同步等待 GPU 完成
│
▼
outTexture.getBytes() → CGContext → CGImage → UIImage(scale, orientation)
背景蒙版 CPU 端生成原理
GraphicAlgorithm.generateSolidMask() 負(fù)責(zé)從圖像像素?cái)?shù)據(jù)中識(shí)別"閉合區(qū)域",生成單通道 Mask。
核心思路
將圖像視為一個(gè)二維網(wǎng)格,非透明像素構(gòu)成"墻壁",透明像素構(gòu)成"通道"。從圖像邊緣開始向內(nèi)泛洪,所有能從邊緣到達(dá)的透明像素都是"外部背景",無法到達(dá)的透明像素則被"墻壁"包圍,屬于"內(nèi)部區(qū)域"。
算法流程
┌─────────────────────────────────────────────────────┐
│ 階段 1: 統(tǒng)計(jì)筆觸面積 │
│ │
│ 遍歷所有像素,統(tǒng)計(jì) alpha > 0 的像素?cái)?shù)量 (strokeArea) │
│ 每隔 5 個(gè)像素采樣坐標(biāo),用于后續(xù)凸包兜底 │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 階段 2: BFS 泛洪填充 │
│ │
│ 1. 初始化: memset(maskData, 255, totalPixels) │
│ 假設(shè)所有像素都是"內(nèi)部有效區(qū)域" │
│ │
│ 2. 種子注入: 掃描圖像四周邊緣 │
│ 如果邊緣像素是透明的 (alpha == 0): │
│ maskData[index] = 0 (標(biāo)記為外部) │
│ 加入 BFS 隊(duì)列 │
│ │
│ 3. BFS 擴(kuò)散: 從種子開始,4 方向遍歷 │
│ 對每個(gè)鄰居: 如果 maskData == 255 且 alpha == 0 │
│ → 標(biāo)記為外部 (maskData = 0),加入隊(duì)列 │
│ 非透明像素 (alpha > 0) 會(huì)阻斷泛洪傳播 │
│ │
│ 結(jié)果: 被非透明像素包圍的透明區(qū)域保持 255 (內(nèi)部) │
│ 與邊緣連通的透明區(qū)域變?yōu)?0 (外部) │
└──────────────────────┬──────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ 階段 3: 校驗(yàn) + 凸包兜底 │
│ │
│ 計(jì)算 fillRatio = maskArea / strokeArea │
│ │
│ 如果 fillRatio < 1.05 (填充面積幾乎沒有增加): │
│ 說明泛洪失敗 (圖形未形成閉合區(qū)域) │
│ 觸發(fā)凸包兜底策略: │
│ 1. 對采樣點(diǎn)計(jì)算 Monotone Chain 凸包 │
│ 2. 用 CoreGraphics 將凸包多邊形光柵化到 maskData │
│ 3. 凸包內(nèi)部 = 255, 外部 = 0 │
└─────────────────────────────────────────────────────┘
BFS 泛洪示意
原始圖像 (. = 透明, # = 非透明): Mask 結(jié)果 (0 = 外部, 1 = 內(nèi)部):
. . . . . . . . 0 0 0 0 0 0 0 0
. . # # # # . . 0 0 1 1 1 1 0 0
. # . . . . # . 0 1 1 1 1 1 1 0
. # . . . . # . → 0 1 1 1 1 1 1 0
. # . . . . # . 0 1 1 1 1 1 1 0
. . # # # # . . 0 0 1 1 1 1 0 0
. . . . . . . . 0 0 0 0 0 0 0 0
邊緣透明像素從四周向內(nèi)泛洪,被 # 圍住的內(nèi)部透明像素?zé)o法被到達(dá),保持為 1。
性能優(yōu)化
- 使用
UnsafeMutablePointer<FloodPoint>分配連續(xù)內(nèi)存作為 BFS 隊(duì)列,避免 Swift Array 的動(dòng)態(tài)擴(kuò)容開銷 -
FloodPoint使用Int16存儲(chǔ)坐標(biāo),節(jié)省內(nèi)存(支持最大 32767×32767 圖像) -
memset初始化 Mask,比逐元素賦值快一個(gè)數(shù)量級 -
@inline(__always)標(biāo)記入隊(duì)輔助函數(shù),消除函數(shù)調(diào)用開銷
GPU Shader 詳解
Pass 1: dilate_mask (Compute)
將基礎(chǔ) Mask 按指定半徑做圓形形態(tài)學(xué)膨脹。
輸入:
texture(0): cpuMaskTexture (r8Unorm) - CPU 生成的基礎(chǔ) Mask
buffer(0): radius (int) - 膨脹半徑
輸出:
texture(1): dilatedMaskTexture (r8Unorm) - 膨脹后的 Mask
算法:
對每個(gè)像素,在半徑 r 的圓形區(qū)域內(nèi)搜索:
如果找到任何 mask > 0.5 的鄰居 → 輸出 1.0
否則 → 輸出 0.0
圓形判定: i2 + j2 ≤ r2
提前退出: 找到有效鄰居后立即 break,避免無效搜索
Pass 2: apply_color_overlay (Compute)
根據(jù)膨脹后的 Mask 和指定顏色生成最終蒙版圖像。
輸入:
texture(0): inTexture (rgba8Unorm) - 原始圖像
texture(1): maskTexture (r8Unorm) - 膨脹后的 Mask
buffer(0): OverlayColor { float4 color } - 疊加顏色 RGBA
輸出:
texture(2): outTexture (rgba8Unorm) - 最終結(jié)果
邏輯:
if mask > 0.5:
if 原始 alpha > 0: → 保留原始像素 (前景筆畫)
else: → 填充指定顏色 (背景蒙版)
else:
→ 全透明 (0,0,0,0) (外部區(qū)域)
Render Pass: quad_vertex_main + quad_fragment_main
將 Compute Shader 的輸出紋理以 Aspect-Fit 方式繪制到 MTKView 的 drawable 上。
Vertex Shader:
輸入: 4 個(gè)頂點(diǎn)坐標(biāo) (NDC) + 4 個(gè)紋理坐標(biāo)
輸出: 裁剪空間坐標(biāo) + 插值紋理坐標(biāo)
繪制方式: Triangle Strip (4 頂點(diǎn) = 1 個(gè)矩形)
Fragment Shader:
輸入: 插值后的紋理坐標(biāo)
采樣: bilinear filtering (mag_filter::linear, min_filter::linear)
輸出: 采樣顏色值
Aspect-Fit 計(jì)算 (Swift 端):
scaleFit = min(viewW/imgW, viewH/imgH, 1.0) // 不放大,只縮小
NDC 范圍 = [-scaleFit*imgW/viewW, +scaleFit*imgW/viewW]
效果: 圖像居中顯示,保持原始寬高比,周圍透明
線程安全
-
NSLock (realtimeLock)保護(hù)緩存紋理的讀寫 -
prepareForRealtimeRendering在鎖內(nèi)完成全部預(yù)處理 -
renderToView在鎖內(nèi)讀取緩存引用,解鎖后執(zhí)行 GPU 渲染 -
applyOverlay使用獨(dú)立的局部紋理,不訪問緩存,與實(shí)時(shí)渲染完全隔離
SwiftUI 集成
MTKViewRepresentable 將 MTKView 包裝為 SwiftUI 視圖:
MTKViewRepresentable(renderer: ColorOverlayRenderer.shared)
配置:
-
isPaused = true+enableSetNeedsDisplay = true: 按需繪制模式 -
colorPixelFormat = .rgba8Unorm: 匹配緩存紋理格式 -
framebufferOnly = false: 支持像素回讀 -
isOpaque = false: 支持透明背景