提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
用HLSL在像素着色器中实现多种形状的马赛克(圆形、方形、三角形、梯形、六边形、心形、五角星形)。之前实现过多种形状的放大镜特效,这次再更新一下多种形状的马赛克特效。放大镜中是对整张纹理的某一个区域进行处理(画出一个形状进行放大),而马赛克的话是将整张纹理分成N*N个矩形区域,对每个矩形区域进行处理。
一、矩形马赛克
矩形马赛克作为最基础的马赛克实现起来也比较简单,主要按照以下思路实现:1、将一整张纹理分成一个个矩形区域。2、处于一个矩形区域的像素统一采用一个点的rgba参数用与输出。
实现代码如下:
//矩形马赛克
float4 RecMosaic(float2 tex)
{
//将当前纹理点从(0~1)的范围恢复到窗口大小(例如:1920*1080)
float2 v2PixelSite
= float2(tex.x * v2TexSize.x, tex.y * v2TexSize.y);
//设置马赛克块的大小
float2 g_v2MosaicSize = float2(Mosaicsize, Mosaicsize);
//通过int取整找到当前点对应的矩形的中心点 取中心点是为了避免图像边缘出现黑边的问题
float2 v2NewUV = float2(int2(v2PixelSite / g_v2MosaicSize) * g_v2MosaicSize) + g_v2MosaicSize / 2;
//将纹理范围恢复回shader中的(0~1)
v2NewUV /= float2(v2TexSize.x, v2TexSize.y);
//采样
float4 outp;
outp = g_Tex.Sample(g_SamLinear, v2NewUV);
return outp;
}
其中:
outp为像素着色器输出的float4 型的rgba参数用于像素着色器输出。
v2TexSize为窗口的大小(例如:float2 v2TexSize = float2(1920,1080)),一开始尝试过用纹理的大小,发现效果有问题,因为窗口大小会随时变化,v2TexSize参数的加入是为了保证当窗口大小发生变化时马赛克块的形状不会发生形变。
Mosaicsize为指定的马赛克块的大小,参数越大单个马赛克就越大,可以通过控制这个参数改变马赛克的大小。
矩形马赛克中的关键点在于 int 型,公式中int函数实际上截断了小数部分.
使用采样器提取纹理时还需要将当前坐标转换回纹理坐标。
效果如下图:
变换前:
变换后:
二、圆形马赛克
圆形马赛克需要取用的公共点的中心点(矩形也是取用的中心点作为公共点),之前操作跟方形马赛克类似,在获取左上角公共坐标后还需要将其位置移动到圆心,
伪代码:mosaic[X,Y] = [int(x/16) * 16 , int(y/16) * 16]+[8,8]; 此处MosaicSize=16,即圆的直径为16.
得到圆心后,计算圆心与当前纹理点与圆心的距离,当距离小于原的半径(当前为8)便采用与圆心相同的纹理,否者采用本身的纹理。
实现代码如下:
//圆形马赛克
float4 CirMosaic(float2 tex)
{
//圆形马赛克
float2 v2PixelSite= float2(tex.x * v2TexSize.x, tex.y * v2TexSize.y);
float2 g_v2MosaicSize = float2(Mosaicsize, Mosaicsize);
//新的纹理坐标取到中心点
float2 v2NewUV
= float2(int(v2PixelSite.x / g_v2MosaicSize.x) * g_v2MosaicSize.x
, int(v2PixelSite.y / g_v2MosaicSize.y) * g_v2MosaicSize.y) + 0.5 * g_v2MosaicSize;
float2 v2DeltaUV = v2NewUV - v2PixelSite; //计算当前点距离圆心的距离
float fDeltanLen = length(v2DeltaUV);
float2 v2MosaicUV = float2(v2NewUV.x / v2TexSize.x, v2NewUV.y / v2TexSize.y);
float4 outp,
//判断新的UV点是否在圆心内
if (fDeltanLen <= (0.5 * Mosaicsize))
{
outp= g_Tex.Sample(g_SamLinear, v2MosaicUV);
}
else//否则显示本身的颜色
{
outp= g_Tex.Sample(g_SamLinear, tex);
}
return outp;
效果:
三、六边形马赛克
这里需要将六边体马赛克、三角形马赛克、梯形马赛克归类在一个系列,后两者与六面体马赛克高度相关。六边体可由六个三角形组成、也可以由两个梯形组成。参考文章:链接
//六边形马赛克(使用正六边形)
float4 HexMosaic(float2 tex)
{
//六边形一条边的长度
float len = float(Mosaicsize / v2TexSize.x);
// 一个正六边形,任意3个连续顶点与中心点连接成的平行四边形的宽高比是 3:根号3, -> 1.5: 0.866025
float TR = 0.866025f; //0.5*根号3
float x = tex.x;
float y = tex.y;
int wx = int(x / 1.5f / len);
int wy = int(y / TR / len);//这两步的作用的找到距离当前点所在的那个矩形
float2 v1, v2, vn;
if (wx / 2 * 2 == wx)
{
if (wy / 2 * 2 == wy)
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy + 1));
}
else
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy + 1));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy));
}
}
else
{
if (wy / 2 * 2 == wy)
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy + 1));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy));
}
else
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy + 1));
}
}
// 每一个梯形都得到v1和v2两个六边形的中心点,
// 计算距离,近则取之
float s1 = sqrt(pow(v1.x - x, 2) + pow(v1.y - y, 2));
float s2 = sqrt(pow(v2.x - x, 2) + pow(v2.y - y, 2));
if (s1 < s2)
{
vn = v1;
}
else
{
vn = v2;
}
float4 outp = g_Tex.Sample(g_SamLinear, vn);
return outp;
效果:
四、梯形马赛克
已经实现了六边形马赛克得话,梯形马赛克得实现就非常简单了,因为一个六边形就是由上下两个梯形组成的。
分别选取上下两个梯形的一个顶点预备。用比较当前点与六边形的中点在Y轴上的大小,小则在上面的梯形,否则在下面的梯形。
//梯形马赛克
float4 TrapMosaic(float2 tex)
{
//六边形一条边的长度
float len = float(Mosaicsize / v2TexSize.x);
float TR = 0.866025f; //0.5*根号3
float PI6 = 0.523599;
float x = tex.x;
float y = tex.y;
int wx = int(x / 1.5 / len);
int wy = int(y / TR / len); //这两步的作用的找到距离当前点所在的那个矩形
float2 v1, v2, vn;
if (wx / 2 * 2 == wx)
{
if (wy / 2 * 2 == wy)
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy + 1));
}
else
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy + 1));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy));
}
}
else
{
if (wy / 2 * 2 == wy)
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy + 1));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy));
}
else
{
v1 = float2(len * 1.5 * float(wx), len * TR * float(wy));
v2 = float2(len * 1.5 * float(wx + 1), len * TR * float(wy + 1));
}
}
float s1 = sqrt(pow(v1.x - x, 2.0) + pow(v1.y - y, 2.0));
float s2 = sqrt(pow(v2.x - x, 2.0) + pow(v2.y - y, 2.0));
if (s1 < s2)
{
vn = v1;
}
else
{
vn = v2;
}
float2 mid = vn; //获得中点位置
float2 top = float2(vn.x - len / 2.0, vn.y - len * TR / 2.0);
float2 back = float2(vn.x + len / 2.0, vn.y + len * TR / 2.0);
//一个六边形由上下两个梯形组成,直接将纹理的左边的Y轴与六边形中点做比较,小则在上面的梯形,否者在下面的梯形
if (tex.y > mid.y)
{
float4 outp = g_Tex.Sample(g_SamLinear, top);
}
else /*if (tex.y <= mid.y)*/
{
float4 outp = g_Tex.Sample(g_SamLinear, back);
}
return outp;
}
效果:
五、三角形马赛克(基于六边形实现)
这种三角形马赛克是在六边形马赛克基础上实现的,一个六边形可分为六个三角形构成,得到六边形马赛克的基础上进一步细分,参考文章:链接
//三角形马赛克 基于六边形马赛克
float4 TriMosaic(float2 tex)
{
//六边形一条边的长度
float len = float(Mosaicsize / v2TexSize.x);
// 一个正六边形,任意3个连续顶点与中心点连接成的平行四边形的宽高比是 3:根号3, -> 1.5: 0.866025
float TR = 0.866025f; //0.5*根号3
float x = tex.x;
float y = tex.y;
//......
//......中间的操作都跟六边形马赛克一样,省略
//......
if (s1 < s2)
{
vn = v1;
}
else
{
vn = v2;
}
float a = atan((x - vn.x) / (y - vn.y));//计算夹角
//求出所在六边形中的六个顶点
//1
float2 area1 = float2(vn.x, vn.y - len * TR / 2.0);
// 2
float2 area2 = float2(vn.x + len / 2.0, vn.y - len * TR / 2.0);
//3
float2 area3 = float2(vn.x + len / 2.0, vn.y + len * TR / 2.0);
//4
float2 area4 = float2(vn.x, vn.y + len * TR / 2.0);
// 5
float2 area5 = float2(vn.x - len / 2.0, vn.y + len * TR / 2.0);
// 6
float2 area6 = float2(vn.x - len / 2.0, vn.y - len * TR / 2.0);
if (a >= PI6 && a < PI6 * 3.0)
{
vn = area1;
}
else if (a >= PI6 * 3.0 && a < PI6 * 5.0)
{
vn = area2;
}
else if ((a >= PI6 * 5.0 && a <= PI6 * 6.0) || (a < -PI6 * 5.0 && a > -PI6 * 6.0))
{
vn = area3;
}
else if (a < -PI6 * 3.0 && a >= -PI6 * 5.0)
{
vn = area4;
}
else if (a <= -PI6 && a > -PI6 * 3.0)
{
vn = area5;
}
else if (a > -PI6 && a < PI6)
{
vn = area6;
}
float4 outp = g_Tex.Sample(g_SamLinear, vn);
return outp;
}
效果:
六、三角形马赛克(基于矩形实现)
居于六边形实现三角形马赛克可能有点复杂,我们完全可以在正方形马赛克的基础上实现。
正方形马赛克的边长为MosaicSize,且我们获得了正方形的中心,于也可得正方形的左右下角。
通过判断当前点距离两个顶点的距离来判断当前点处于哪个三角形半区。
//三角形马赛克(正方形实现)
float4 TriMosaicT(float2 tex)
{
float2 v2PixelSite
= float2(tex.x * v2TexSize.x, tex.y * v2TexSize.y);
float2 g_v2MosaicSize = float2(Mosaicsize, Mosaicsize);
//通过int取整找到当前点对应的矩形左上角的点
float2 v2NewUV
= float2(int(v2PixelSite.x / g_v2MosaicSize.x) * g_v2MosaicSize.x
, int(v2PixelSite.y / g_v2MosaicSize.y) * g_v2MosaicSize.y);
float2 v2NewUV2 = float2(v2NewUV.x + Mosaicsize, v2NewUV.y + Mosaicsize);
float s1 = sqrt(pow(v2NewUV.x - v2PixelSite.x, 2.0) + pow(v2NewUV.y - v2PixelSite.y, 2.0));
float s2 = sqrt(pow(v2NewUV2.x - v2PixelSite.x, 2.0) + pow(v2NewUV2.y - v2PixelSite.y, 2.0));
if (s1 < s2)
{
v2NewUV /= float2(v2TexSize.x, v2TexSize.y);
float4 outp = g_Tex.Sample(g_SamLinear, v2NewUV);
}
else
{
v2NewUV2 /= float2(v2TexSize.x, v2TexSize.y);
float4 outp = g_Tex.Sample(g_SamLinear, v2NewUV2);
}
return outp;
}
效果:
七、心形马赛克
思路:将整张纹理分成一个个矩形区域,在每个矩形中画出一块心形区域。对当前这个矩形区域的纹理点进行判断,在心形区域内的取心形中心点纹理rgba。否则取自己本身的rgba输出。
float4 HeartMosaic(float2 tex)
{
tex *= v2TexSize;
float2 g_v2MosaicSize = float2(Mosaicsize, Mosaicsize);
//通过int取整找到当前点对应的矩形左上角的点
float2 center
= float2(int(tex.x / g_v2MosaicSize.x) * g_v2MosaicSize.x
, int(tex.y / g_v2MosaicSize.y) * g_v2MosaicSize.y);
center += g_v2MosaicSize / 2.; //获取矩形的中心
tex -= center;
if (inHeart(tex, Mosaicsize/2.4))//关于这个2.4的取值,是试出来的,大家可以尝试修改一下这个值看看效果
{
outp = g_Tex.Sample(g_SamLinear, center / v2TexSize);
}
return outp;
}
其中的inHeart函数在上一篇多形状放大镜中给出了实现。主要目的是判断当前纹理点是否在所处矩形区域中的心形区域内。
效果如下:
原图:
心形马赛克效果:
八、五角星形马赛克
这个五角星马赛克的遮挡效果其实相当糟糕,因为是采用之前马赛克一贯的思路,在一个矩形区域画出一个五角星区域。但是很明显,一个五角星难以遮挡一个矩形区域的许多部分。所以实现这个五角星马赛克主要是练习一下。
float4 starMosaic(float2 tex)
{
tex *= v2TexSize;
float2 g_v2MosaicSize = float2(Mosaicsize, Mosaicsize);
//通过int取整找到当前点对应的矩形左上角的点
float2 center
= float2(int(tex.x / g_v2MosaicSize.x) * g_v2MosaicSize.x
, int(tex.y / g_v2MosaicSize.y) * g_v2MosaicSize.y);
center += g_v2MosaicSize / 2.; //获取矩形的中心,即圆心
float R = g_v2MosaicSize/2;//外五点半径
float r = R * sin(PI / 10.) / cos(PI / 5.);//内五点半径
tex -= center;
//float x0, y0, x1, y1;
//float o0 = 0.1 * PI;//大圆角度
//float o1 = 0.1 * PI+0.2*PI; //小圆角度
float2 PointO[5]; //外五角
float2 PointI[5]; //内五角
for (int i = 0; i < 5; i++)
{
PointO[i] = float2(R * cos(0.5 * PI + i * 0.4 * PI), -R * sin(0.5 * PI + i * 0.4 * PI));
PointI[i] = float2(r * cos(0.7 * PI + i * 0.4 * PI), -r * sin(0.7 * PI + i * 0.4 * PI));
if (inTriangle(float2(0, 0), PointO[i], PointI[i], tex))
{
float4 outp = g_Tex.Sample(g_SamLinear, center / v2TexSize);
}
}
if (inTriangle(float2(0, 0), PointO[0], PointI[4], tex))
{
float4 outp = g_Tex.Sample(g_SamLinear, center / v2TexSize);
}
for (int j = 1; j < 5; j++)
{
if (inTriangle(float2(0, 0), PointO[j], PointI[j - 1], tex))
{
float4 outp = g_Tex.Sample(g_SamLinear, center / v2TexSize);
}
}
tex += center;
return outp;
}
其中的inTriangle函数在上一篇多形状放大镜中给出了实现。主要目的是判断当前纹理点是否在由三个点形成的三角形区域内。
效果如下:
总结
其实这些特效的实现都是挺久之前的事了,代码里面还有很多的优化以及化简的空间,但现在因为工作没时间修改。主要目的是分享一下实现的思路给大家。