思路一:刚开始的想法是把物体放大,放大的物体全设为白色,就是不规则的物体中心点很难找
思路二:如果能找到边缘,按边缘的法线方向放大一定比例就可以,可是怎么找到边缘,又怎么找到法线方向,一时无从入手
思路三:假设知道物体的所有点到物体中某一点的距离,只要变换距离,就能放大物体,从而实现描边,那要怎么算出这个距离呢?原来在计算机图形学中这个距离叫有向距离场,又叫sdf(signed Distance Field)。
如上图中有一个半径3.3的圆,圆的边缘是0,中心就是-3.3,所有点的距离构成了有向距离场,如果想把这个圆放大一倍的话,只需要改变距离就可以,对于不规则物体一样生效。
那现在的问题就是如何算出不规则物体的有向距离场
举个例子,求五角星的有向距离场
先设一个任意点A
求A点到五角星每个点的最小距离
这张图中所有点的最小距离算上方向构成了有向距离场,用有向距离场换算成对应的颜色就生成了上图。
有向距离场生成
单通道有向距离场生成
算每个点到边缘的最小距离不能每个点和所有点都比较一次,这里我们选点周围md*md的矩形减小计算量。
计算步骤如下:
先算出当前点在物体内部还是外部
然后找到周围md*md的点和当前点不在同一边的所有点,算出这些点到当前点的最小距离
算出的最小距离归一化加上方向就是有向距离场
void
sdfColor
(out vec4 color, in vec2 uv)
float
a
=
texture(iChannel0, uv).a;
bool
i
=
bool(step(
0.5
, a) ==
1.0
);
const
int
md
=
20
;
const
int
h_md
=
md /
2
;
float
d
=
float
(md);
for
(
int
x
=
-h_md; x != h_md; ++x)
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
;
vec4
color
=
vec4(d);
result = color;
该算法的问题在于矩形大一点计算量就非常大,假设原图是w
h,计算的矩形md
md,那计算量就是w
h
md*md,这会导致计算很慢,而矩形不大算出的有向距离场的精度和范围就非常小。
双通道有向距离场生成
如何降低计算量呢?可以这样思考,距离就是宽和高决定的,要找到最短距离,我只要找到最短宽,再从最短宽的计算结果中找到最短高就可以了,最终就是先按水平方向算出最小宽度,再用这个结果按垂直方向算一遍最小高度,最终计算量从w
h
md
md变成了w
h*(md+md),计算量大大降低了。
float source(vec2 uv)
return texture2D(inputImageTexture,uv).a-0.5;
float s = sign(source(uv));
float d = 0.;
for(int i= 0; i < distance; i++){
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;
gl_FragColor =vec4(vec3(dMin),1.0);
float sd(vec2 uv)
float x = texture2D(inputImageTexture, uv).x;
return (x-0.5)*2.0*distance;
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;
d =d*0.5+0.5;
gl_FragColor =vec4(vec3(d),1.0);
该描边从边缘往外面生长,生成的有向距离场是-1->1,只取外描边,需要截掉-1->0,描边强度是outlineWidth(0->1),那么只要小于等于outlineWidth显示就可以了
float getOutlineMask(){
float d = sd(textureCoordinate);
float b = outlineWidth;
return step(d, b);
该描边从中间外两边生长,最大的时候描边也只显示一半,有向距离场的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);
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);
float b = outlineWidth;
return smoothstep(0.0,1.0,1.0-clamp(d/b,0.0,1.0));
具体代码在GLRenderClient:github.com/JonaNorman/…
该代码库是一个面向Android开发的OpenGL渲染库,致力于帮助广大Android开发者降低开发成本,它可以使用在图片编辑、视频图像编辑中。
已经实现以下功能:
自动解析shader参数
异步渲染Android原生的View
多层级多时间线渲染
特效嵌套渲染
shader参数关键帧
View特效
obj文件渲染
高斯模糊特效
欢迎大家提意见给我。