OpenGL之仿美图实现不规则物体加描边特效

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

图片

实现

分析

思路一:刚开始的想法是把物体放大,放大的物体全设为白色,就是不规则的物体中心点很难找

思路二:如果能找到边缘,按边缘的法线方向放大一定比例就可以,可是怎么找到边缘,又怎么找到法线方向,一时无从入手

思路三:假设知道物体的所有点到物体中某一点的距离,只要变换距离,就能放大物体,从而实现描边,那要怎么算出这个距离呢?原来在计算机图形学中这个距离叫有向距离场,又叫sdf(signed Distance Field)。

图片

如上图中有一个半径3.3的圆,圆的边缘是0,中心就是-3.3,所有点的距离构成了有向距离场,如果想把这个圆放大一倍的话,只需要改变距离就可以,对于不规则物体一样生效。

那现在的问题就是如何算出不规则物体的有向距离场

举个例子,求五角星的有向距离场

先设一个任意点A

求A点到五角星每个点的最小距离

图片

这张图中所有点的最小距离算上方向构成了有向距离场,用有向距离场换算成对应的颜色就生成了上图。

有向距离场生成

单通道有向距离场生成

算每个点到边缘的最小距离不能每个点和所有点都比较一次,这里我们选点周围md*md的矩形减小计算量。计算步骤如下:

  1. 先算出当前点在物体内部还是外部
  2. 然后找到周围md*md的点和当前点不在同一边的所有点,算出这些点到当前点的最小距离
  3. 算出的最小距离归一化加上方向就是有向距离场

void sdfColor(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算物体内部
    const int md = 20;//当前点周围md*md个点
    const int 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),计算量大大降低了。

水平方向

float source(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);

垂直方向

float sd(vec2 uv)
{
    float x = texture2D(inputImageTexture, uv).x;
    return (x-0.5)*2.0*distance;//把上一个水平方向shader的结果换算回来
}

void main()
{
    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显示就可以了

float getOutlineMask(){
    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实际试一下其实很简单。

float getOutlineMask(){
    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

float getOutlineMask(){
    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

最后欢迎大家加入 音视频开发进阶 知识星球 ,这里有知识干货、编程答疑、开发教程,还有很多精彩分享。

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论