前言
- 非真實感渲染的風格不經相同,其中一種便是油畫風格,本文總結了如何實作油畫濾鏡的方法

Kuwahara Filter
-
為什么使用Kuwahara Filter?
一般對影像進行模糊處理,會使用低通濾波器,但往往模糊后影像會失去它們的硬邊,但Kuwahara Filter可以在平滑影像的同時也能保留其硬邊


-
如何實作Kuwahara Filter?
-
Kuwahara Filter也是使用卷積,但不同之處是Kuwahara Filter需要四個卷積核

-
程序:計算每個卷積核的平均值(平滑噪點)和方差(衡量一個內核的顏色變化率),一共四個,找出方差最小的卷積核并輸出其平均值
-
例子

對于上圖的計算動圖如下

右邊的顏色變化率太大了,不會選它,這里選擇的是最左邊的卷積核,因為它的顏色最均勻,最后輸出它的平均值
-
實作油畫濾鏡
-
根節點需要選擇"Material Domain"為"Post Process"

-
總體實作框架

-
"Global"自定義節點計算平均值和方差
方差計算公式

int32 SceneTextureLookup ( int32 ViewportUV, // 紋理坐標 uint32 SceneTextureId, // 節點sceneTexture中的Scene Texture Id索引值 bool bFiltered //是否使用雙線性插值 )
-
計算四個卷積核

-
效果對比


實作方向性油畫濾鏡
-
為什么需要方向性油畫濾鏡
從上圖可以看出該濾鏡某些地方有點奇怪,某些地方過于方正,而方向性油畫濾鏡可以解決這個問題 -
如何實作
-
方向性油畫濾鏡和之前的差別在于它的卷積核和像素的區域朝向相同

-
計算區域朝向的方法是Sobel
Sobel需要兩個卷積核,Gx提供水平方向的梯度資訊,Gy提供垂直方向的梯度資訊,使用這兩個卷積核分別對像素做一次卷積,再使用atan()求角度,隨后以該角度對卷積核進行旋轉

-
例子

對上圖進行Sobel,得到的結果如下

使用atan()求角度

-
-
具體實作
-
求角度

-
修改GetKernelMeanAndVariance()
float4 GetKernelMeanAndVariance(float2 uv, float4 range, float2x2 rotationMatrix) { //... float2 offset = mul(float2(x, y) * textelSize, rotationMatrix); -
計算旋轉矩陣

-
效果對比


-
源代碼
-
global
float4 GetKernelMeanAndVariance(float2 uv, float4 range, float2x2 rotationMatrix) { float2 textelSize = View.ViewSizeAndInvSize.zw; //紋素大小 const int ppInput0 = 14; //對應SceneTexture的節點索引值 float3 mean = 0; //平均值 float3 variance = 0; //方差 int sampleNums = 0; //采樣次數 for(int x = range.x; x <= range.y; ++x) { for(int y = range.z; y <= range.w; ++y) { float2 offset = mul(float2(x, y) * textelSize, rotationMatrix); float3 pixelColor = SceneTextureLookup(uv + offset, ppInput0, false).rgb; mean += pixelColor; variance = pixelColor * pixelColor; sampleNums++; } } mean /= sampleNums; variance = variance / sampleNums - mean * mean; float totalVariance = variance.r + variance.g + variance.b; return float4(mean.r, mean.g, mean.b, totalVariance); } // 求角度 float4 GetAngle(float2 uv) { float2 textelSize = View.ViewSizeAndInvSize.zw; //紋素大小 const int ppInput0 = 14; //對應SceneTexture的節點索引值 float gradientX = 0.f; // 水平方向的梯度值 float gradientY = 0.f; // 豎直方向的梯度值 float sobelX[9] = {-1, -2, -1, 0, 0, 0, 1, 2, 1}; // 水平方向的卷積核 float sobelY[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1}; // 垂直方向的卷積核 int i = 0; //訪問sobel的索引 for(int x = -1; x <= 1; ++x) { for(int y = -1; y <= 1; ++y) { float2 offset = float2(x, y) * textelSize; float3 pixelColor = SceneTextureLookup(uv + offset, ppInput0, false).rgb; float pixelValue = https://www.cnblogs.com/chenglixue/archive/2023/05/29/dot(pixelColor, float3(0.3,0.59,0.11)); // 轉化為灰度值,用于將影像看作一個整體計算梯度,比計算單個顏色值的梯度快 // 計算梯度值 gradientX += pixelValue * sobelX[i]; gradientY += pixelValue * sobelY[i]; i++; } } return atan(gradientY / gradientX); -
Kuwahara
const int ppInput0 = 14; float2 uv = GetDefaultSceneTextureUV(Parameters, ppInput0); //目標像素點 float4 range; //卷積核范圍.xy表示x的范圍,zw表示y的范圍 float4 meanAndVariance[4]; //算得的平均值和方差 float angle = GetAngle(uv); float2x2 rotationMatrix = float2x2(cos(angle), -sin(angle), sin(angle), cos(angle)); // 計算四個卷積核 range = float4(-RadiusX, 0, -RadiusY, 0); meanAndVariance[0] = GetKernelMeanAndVariance(uv, range, rotationMatrix); range = float4(-RadiusX, 0, 0, RadiusY); meanAndVariance[1] = GetKernelMeanAndVariance(uv, range, rotationMatrix); range = float4(0, RadiusX, 0, RadiusY); meanAndVariance[2] = GetKernelMeanAndVariance(uv, range, rotationMatrix); range = float4(0, RadiusX, -RadiusY, 0); meanAndVariance[3] = GetKernelMeanAndVariance(uv, range, rotationMatrix); // 求方差最小值的顏色 float3 finalColor = meanAndVariance[0].rgb; float minVariance = meanAndVariance[0].a; for(int i = 1; i < 4; ++i) { if(minVariance > meanAndVariance[i].a) { minVariance = meanAndVariance[i].a; finalColor = meanAndVariance[i].rgb; } } return finalColor;
reference
UE4卡通渲染基礎教程 Part4:Paint Filter - 知乎 (zhihu.com)
Unreal Engine 4 Paint Filter Tutorial | Kodeco
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/553769.html
標籤:其他
下一篇:返回列表
