背景

在美图中有个功能可以给抠图以后的物体加上描边,就想着能不能在Android中用OpenGL实现它,最终效果如下:

实现

分析

思路一:刚开始的想法是把物体放大,放大的物体全设为白色,就是不规则的物体中心点很难找
思路二:如果能找到边缘,按边缘的法线方向放大一定比例就可以,可是怎么找到边缘,又怎么找到法线方向,一时无从入手
思路三:假设知道物体的所有点到物体中某一点的距离,只要变换距离,就能放大物体,从而实现描边,那要怎么算出这个距离呢?原来在计算机图形学中这个距离叫有向距离场,又叫sdf(signed Distance Field)。
如上图中有一个半径3.3的圆,圆的边缘是0,中心就是-3.3,所有点的距离构成了有向距离场,如果想把这个圆放大一倍的话,只需要改变距离就可以,对于不规则物体一样生效。
那现在的问题就是如何算出不规则物体的有向距离场
举个例子,求五角星的有向距离场
先设一个任意点A
求A点到五角星每个点的最小距离
这张图中所有点的最小距离算上方向构成了有向距离场,用有向距离场换算成对应的颜色就生成了上图。

有向距离场生成

单通道有向距离场生成

算每个点到边缘的最小距离不能每个点和所有点都比较一次,这里我们选点周围md*md的矩形减小计算量。计算步骤如下:
  1. 先算出当前点在物体内部还是外部
  2. 然后找到周围md*md的点和当前点不在同一边的所有点,算出这些点到当前点的最小距离
  3. 算出的最小距离归一化加上方向就是有向距离场

voidsdfColor(out vec4 color, in vec2 uv)// color是最终计算的有向距离场代表的颜色,uv就是每个点的坐标
{

float
 a = texture(iChannel0, uv).a;
//求当前点的透明度
bool
 i = 
bool
(step(
0.5
, a) == 
1.0
);
//透明度大于等于0.5算物体内部
constint
 md = 
20
;
//当前点周围md*md个点
constint
 h_md = md / 
2
;

float
 d = 
float
(md);

for
 (
int
 x = -h_md; x != h_md; ++x)
//当前点周围md*md个点
    {

for
 (
int
 y = -h_md; y != h_md; ++y)

        {

            vec2 o = vec2(
float
(x), 
float
(y));
//当前点周围点的坐标偏移
            vec2 s = uv+o;

float
 o_a = texture(iChannel0, s).a;

bool
 o_i = 
bool
(step(
0.5
, o_a) == 
1.0
);
//周围点是否也是物体内部
if
 (!i && o_i || i && !o_i)
//如果当前点和周围点不在物体的同一边
                d = min(d, length(o));
//最小距离
        }

    }


    d = clamp(d, 
0.0
float
(md)) / 
float
(md);
//归一化
if
 (i)

        d = -d;
//算上方向
    d = d * 
0.5
 + 
0.5
;
// -1->1换算成0->1,因为颜色是0->1
    vec4 color = vec4(d);
//用距离代表颜色,可视化算出的有向距离场
    result = color;

}

该算法的问题在于矩形大一点计算量就非常大,假设原图是wh,计算的矩形mdmd,那计算量就是whmd*md,这会导致计算很慢,而矩形不大算出的有向距离场的精度和范围就非常小。

双通道有向距离场生成

如何降低计算量呢?可以这样思考,距离就是宽和高决定的,要找到最短距离,我只要找到最短宽,再从最短宽的计算结果中找到最短高就可以了,最终就是先按水平方向算出最小宽度,再用这个结果按垂直方向算一遍最小高度,最终计算量从whmdmd变成了wh*(md+md),计算量大大降低了。

水平方向

floatsource(vec2 uv)
{

return
 texture2D(inputImageTexture,uv).a
-0.5
//0.5作为阈值,大于0.5算物体内部
}


float
 s = sign(source(uv));
//sign方法知道数字的正负
float
 d = 
0.
;   

for
(
int
 i= 
0
; i < distance; i++){

    d++;

    vec2 offset =  vec2(d * textureStep.x, 
0.
);
//只找当前点的左右两边
if
(s * source(uv + offset) < 
0.
)
break
;
//不在同一边就算找到了
if
(s * source(uv - offset) < 
0.
)
break

}


float
 sd = s*d;
//距离*方向
float
 dMin =sd/distance*
0.5
+
0.5
;
//-distance->distance换算成0-1,因为最小宽度要给到后面的shader,shader的color只识别0-1,不然数据会丢失
gl_FragColor =vec4(vec3(dMin),
1.0
);

垂直方向

floatsd(vec2 uv)
{

float
 x = texture2D(inputImageTexture, uv).x;

return
 (x
-0.5
)*
2.0
*distance;
//把上一个水平方向shader的结果换算回来
}


voidmain()
{

float
 dx = sd(uv);
//带方向的最小宽度
float
 dMin = 
abs
(dx);
//最小宽度
float
 dy = 
0.
;
//
for
(
int
 i= 
0
; i < distance; i++){

        dy += 
1.
;

        vec2 offset =  vec2(
0.
, dy * textureStep.y);

float
 dx1 = sd(uv + offset);
//找下方
if
(dx1 * dx < 
0.
){
//下方的点和当前点不在同一边
            dMin = dy;
//最小高度就是最短距离
break
;

        }

        dMin = min(dMin, length (vec2(dx1, dy)));
//计算最小高度和最小宽度形成的最短距离
float
 dx2 = sd(uv - offset);
//找上方
if
(dx2 * dx < 
0.
){
//上方的点和当前点不在同一边
            dMin = dy;
//最小高度就是最短距离
break
;

        }

        dMin = min(dMin, length (vec2(dx2, dy)));

if
(dy > dMin)
break
;
//
        }


        dMin *= sign(dx);
//最短距离
float
 d = dMin/D;
//-1->1
        d =d*
0.5
+
0.5
;
//归一化-1->1换算成0->1
        gl_FragColor =vec4(vec3(d),
1.0
);

}

描边生成

描边一

该描边从边缘往外面生长,生成的有向距离场是-1->1,只取外描边,需要截掉-1->0,描边强度是outlineWidth(0->1),那么只要小于等于outlineWidth显示就可以了
floatgetOutlineMask()
{

float
 d = sd(textureCoordinate);
//当前点的有向距离0->1,已经去掉-1->0
float
 b =   outlineWidth;

return
 step(d, b);
//step表示b>=d是1,1表示显示
}

描边二

该描边从中间外两边生长,最大的时候描边也只显示一半,有向距离场的0->1,中间就是0.5,两边效果是一样的取绝对值abs(d-0.5),减去0.5以后大小变成了一半,需要放大一倍,abs(d-0.5)*2.0,该效果最大也只显示一半,所以outlineWidth要乘以0.5,最终得到step(abs(d-0.5)2.0,b0.5),听起来有点绕,跑个demo实际试一下其实很简单。
floatgetOutlineMask()
{

float
 d = sd(textureCoordinate);
//当前点的有向距离0->1,已经去掉-1->0
float
 b =   outlineWidth;

return
 step(
abs
(d
-0.5
)*
2.0
,b*
0.5
);
// 
}

描边三

该描边边缘慢慢消失,需要用smoothstep实现渐变,该描边的最外面代表1的话,就需要归一 d/outlineWidth,而该描边的方向是和有向距离的方向是反的,需要1.0-d/outlineWidth
floatgetOutlineMask()
{

float
 d = sd(textureCoordinate);
//当前点的有向距离0->1,已经去掉-1->0
float
 b =   outlineWidth;

return
 smoothstep(
0.0
,
1.0
,
1.0
-clamp(d/b,
0.0
,
1.0
));
// 
}

代码

具体代码在 https://github.com/JonaNorman/GLRenderClient
来源:https://juejin.cn/post/7168502592249692197
最后欢迎大家加入 音视频开发进阶 知识星球,这里有知识干货、编程答疑、开发教程,还有很多精彩分享。
更多内容可以在星球菜单中找到,随着时间推移,干货也会越来越多!!!
给出 10元 优惠券,涨价在即,目前还是白菜价,基本上提几个问题就回本,投资自己就是最好的投资!!!
加我微信 ezglumes ,拉你进技术交流群
推荐阅读:
觉得不错,点个在看呗~
继续阅读
阅读原文