接下來幾篇文章我們將稍微簡單的探索下PS中多種圖層混合模式的演算法內部原理,因為畢竟沒有這方面的官方資料,所以很多方面也只是本人自己的探索和實踐,有可能和實際的情況有著較大的差異。
在PS的實踐中,圖層樣式的存在使得一個簡單的圖形蛻變為一個豐富的樣式成為可能,而在PS的各個版本中,圖層樣式的選項也越來越豐富, 功能也越來越強大。作為一個成功的圖形和圖形編輯軟體,圖層樣式功能是否缺失也可以看成其是否具有強大生命力的一個典型標誌,比如作為影像開源界的扛把子 GIMP 就沒有這個功能。而平時我們能看到的商業軟體也鮮有這個功能。
在我使用的CS6版本的PS中,提供了斜面和浮雕、描邊、內陰影、內發光、光澤、顏色疊加、漸變疊加、圖案疊加、外發光、投影等10中圖層樣式,在我後續的文章中將分別講述除了 外發光和內發光 之外的其他8種樣式的原理和實現。
本文將簡單講述投影樣式的原理,投影樣式的可控引數介面如下所示:
引數包含了混合模式、不透明度、角度、距離、擴充套件、大小‘等高線、消除鋸齒、雜色等。我們先從大的方向開始講起。
在PS中,如果我們開啟一幅JPG影像(一般為RGB格式的),我們會發現PS為該影像所其的名字為背景層,而且層右側有一個鎖的符號,如下所示:
如果此時我們雙擊這個層,出現的是新建圖層的介面,而不是圖層樣式的旋向,如上圖所示。
但是,如果我們開啟的是一副帶透明通道的32位PNG影像,此時系統預設就是用圖層0為該影像命名,而且後側沒有鎖的符號。
此時雙擊圖層符號,則開啟了圖層樣式對話方塊。
通過這個現象可以做個簡單的猜測,圖層樣式需要Alpha通道,而實際的研究也表明,大部分的圖層樣式(除顏色疊加、漸變疊加、圖案疊加,我局的應該把他們從樣式中開除)都是對Alpha通道的資料進行一定處理後,再配合某種顏色和原圖進行一定程度的融合。
完美甚至可以沿用另外一種流行的說法,圖層樣式其內在實際上是按照一定的規則虛擬了1個或幾個圖層,然後通過不同的圖層位置(位於上部或下部)、混合樣式、不透明度等和原圖進行混合。這個也是所有的樣式裡的混合模式、不透明度的概念源頭所在。
再次回到這個投影樣式吧。 在PS裡隨意的弄下這個效果,可以直觀的感覺到這個樣式的作用是根據所選引數在當前層下部虛擬一個陰影層。
那麼我的實現思路核心如下:
第一步: 按照指定的角度將原圖的Alpha資訊偏移一定的角度,偏移後無效的區域Alpha設定為0。
原始影像 原始影像的Alpha通道資訊 按照指定的角度偏離後的Alpha資訊(角度30, 距離20)
簡單的程式碼如下所示:
float SinV = -sinf(Angle / 180.0 * 3.1415926f); float CosV = cosf(Angle / 180.0 * 3.1415926f); int Left = (int)(Distance * CosV + 0.499999f); int Top = (int)(Distance * SinV + 0.499999f); // 計算Alpha通道的偏移資訊 for (int Y = 0; Y < Height; Y++) { int NewY = Y + Top; if ((NewY < 0) || (NewY >= Height)) { memset(ShiftA + Y * Width, 0, Width); } else { unsigned char *LinePD = ShiftA + Y * Width; for (int X = 0; X < Width; X++) { int NewX = X + Left; if ((NewX < 0) || (NewX >= Width)) { LinePD[X] = 0; } else { int Index = NewY * Stride + NewX * 4 + 3; LinePD[X] = Src[Index]; } } } }
介面中的角度和距離共同決定了這個Alpha通道偏離的程度。
對面後面的大小和擴充套件引數,我們結合網路中的一些參考資料,通過本人的實踐,基本上可以確定是使用的如下演算法。
首先我們把大小設定為10,然後把擴充套件設定為100%,對於上面的圖,可達到如下效果:
大小為10,擴充套件為100%時的結果 大小為0時的結果
可以看到,當大小為10,擴充套件100%時,陰影部分變的更為粗大,通過測試,我們發現這個實際上應該是對前述偏移後的Alpha選區進行了一定程度的圓形最大值演算法,我們是是圓形,我們可以比較下同樣半徑的圓形和矩形最大值的結果區別:
半徑為10的矩形最大值 半徑為10的圓形最大值
很明顯的可以看到,矩形最值不能保留原來光滑的圓角,而圓形可以。
因此,我們推測擴充套件就是對選區進行圓形的最大值演算法,而最大值的半徑和大小以及擴充套件的資料有關,根據PS介面擴充套件後面的% 百分比可以認定他為大小的 百分比。
而大小引數,明顯可以看到,隨著大小的變大,陰影越來越模糊,因此,可以猜測這個為對Alpha進行模糊。不過我測試所,似乎並不是高斯模糊,不曉得實際為何種模糊。
// 第二步對這個Alpha進行下堵窒,演算法上就是圓形的最大值演算法 int ChokeSize = (Size * Choke + 49) / 100; if (ChokeSize != 0) // 堵窒 { Status = IM_MaxFilter_Round_Gray(ShiftA, ShiftA, Width, Height, Width, ChokeSize); if (Status != IM_STATUS_OK) goto FreeMemory; } // 第三步對Alpha進行羽化了,高斯模糊(但是PS的不曉得屬於那種模糊) if ((Size != 0) && (Size != ChokeSize)) { Status = IM_GaussBlur(ShiftA, ShiftA, Width, Height, Width, Size - ChokeSize); if (Status != IM_STATUS_OK) goto FreeMemory; }
那麼下面還有一個關鍵的東西,就是那個等高線,這個東西網路上把他說的好神奇,各路大神都有發表感言。我看啊,都是假神,那個東西其實就是如他表面所表現出來的東西,就是一個曲線調整,而且和PS本身的曲線也是一個意思,只不過他調整的不是影像裡的RGB,而是這裡的Alpha,通過動態調整這個Alpha獲得不同的結果。
// 第四步對選區進行等高線演算法,實際上就是一個查表 for (int Y = 0; Y < Height * Width; Y++) { ShiftA[Y] = Table[ShiftA[Y]]; }
那麼最後一步,就是根據不透明度、混合模式以及使用者提供的背景色來建立一個新的圖層,這個圖層位於當前層下方,進行圖層混合了。如果是一個單獨的圖層,由於這個圖層下面沒有其他圖層,混合樣式在這裡其實是起不到作用的(除了那個另類的溶解),這個時候一個簡單的混合程式碼如下所示:
for (int Y = 0; Y < Height; Y++) { unsigned char *LinePD = Dest + Y * Stride; unsigned char *LinePS = Src + Y * Stride; unsigned char *LinePA = ShiftA + Y * Width; for (int X = 0; X < Width; X++) { int B1 = BackColor_B, G1 = BackColor_G, R1 = BackColor_R, A1 = LinePA[X]; int B2 = LinePS[0], G2 = LinePS[1], R2 = LinePS[2], A2 = LinePS[3]; int NewA1 = A1 * Opacity; int BlendAlpha = IM_Div255(A2 * NewA1); int Alpha = A2 * 255 + NewA1 - BlendAlpha; if (Alpha != 0) { LinePD[0] = (B1 * NewA1 + B2 * A2 * 255 - BlendAlpha * B1) / Alpha; LinePD[1] = (G1 * NewA1 + G2 * A2 * 255 - BlendAlpha * G1) / Alpha; LinePD[2] = (R1 * NewA1 + R2 * A2 * 255 - BlendAlpha * R1) / Alpha; } else { LinePD[0] = LinePS[0]; LinePD[1] = LinePS[1]; LinePD[2] = LinePS[2]; } LinePD[3] = IM_Div255(Alpha); LinePS += 4; LinePD += 4; } }
注意這裡的混合的Alpha需要改變。
至於介面裡的消除鋸齒應該是針對曲線的,這個就是在曲線插值時加上抗鋸齒功能,那個什麼雜色之類的無所謂,就是在Alpha資訊里加上一些隨機噪音。沒啥好難的。
當然,經過一些其他測試,發現PS裡的投影還有一些更為複雜的邏輯,和本文的講述不一致,但是本文的效果也在一定程度上能區域性復原結果,對於一些普通的應用是足以完成任務了
提供一個連結工大家測試:https://files.cnblogs.com/files/Imageshop/DropShadow.rar
如果想時刻關注本人的最新文章,也可關注公眾號: