Android OpenGL 做了一个修图(P 图)功能,对标 PS

10,382 阅读5分钟

Android OpenGL 实现 P 图功能

P 图功能与 OpenGL

玩过 P 图软件的朋友一定对这个功能有所了解,P 图我们可以简单地看做把一个区域的像素按照某一方向进行移动,产生一定形变效果,基于这个原理,我们可以手动实现瘦脸、长腿、瘦腰、大眼、丰胸等等一系列效果,从而达到美颜、美型的目的。

P 图功能与 OpenGL

我们将一个区域的像素移走以后,那么用什么来填充这个被"掏空"的区域呢?答案是, OpenGL 自带插值功能会使用周围的像素对被"掏空"的区域进行插值填充。

回想下 OpenGL 纹理贴图,将图像贴到相对大的区域,就会产生拉伸的效果,贴到相对更小的区域就会产生挤压的效果,这都是借助于 OpenGL 的双线性插值算法实现。

对纹理贴图不了解的同学可以移步:Android OpenGL ES 系统性学习教程

所以,当我们选中一块图像区域进行移动时,OpenGL 纹理贴图时会在移动的方向上产生挤压的效果,而反方向便会产生拉伸效果,从而可以实现对人体部位形变效果。

OpenGL 实现 P 图功能

根据上节讨论的原理,我们把选定位图像区域看成一个圆形,圆形之外的区域不进行偏移形变(不受影响),圆内的区域的像素则是越靠近圆心移动位移相对越大。

OpenGL 实现 P 图功能

如上图所示,BC 表示偏移方向和偏移程度的向量,将圆内的所有像素按照向量 BC 的方向进行一定程度的偏移,像素偏移的强度,和像素与圆心的距离相关,越靠近圆心强度越大。

纹理映射1.png

再回想下纹理贴图(纹理映射)那篇文章,我们只是将图像映射到一个网格(2个三角形组成),这是我们只能对整图做形变,无法做到对如脸部等一小块具体的区域做形变。

device-2021-11-08-185457_1.png

以此类比,这个时候我们就需要更多的网格,当网格足够密集,就可以覆盖整张图的全部区域,这个时候我们通过调整网格,可以实现对任意区域的形变,从而手动实现瘦脸、长腿、瘦腰、大眼、丰胸等等效果不在话下。

生成更多的网格实际上是为了能控制一小块网格区域图像的形变,也就是一定范围内网格区域图像的形变,不对这个范围外的图像产生影响。

Android OpenGL 实现 P 图功能

所以,剩下来的问题就是生成很多网格,然后控制网格结点的偏移,通过简单纹理映射实现 P 图功能。

生成网格及对应的顶点坐标和纹理坐标:

/**
 *
 * @param verticalNum  垂直方向网格数
 * @param horizonNum   水平方向网格数
 * @param ppVertices   顶点坐标集合
 * @param ppTexCoor    纹理坐标集合
 */
void GLRender::GenVertexMesh(int verticalNum, int horizonNum, float **ppVertices, float **ppTexCoor)
{
	*ppVertices = static_cast<float *>(malloc(verticalNum * horizonNum * 18 * sizeof(float)));
	*ppTexCoor = static_cast<float *>(malloc(verticalNum * horizonNum * 12 * sizeof(float)));
	m_pStatusTexCoords = static_cast<float *>(malloc(verticalNum * horizonNum * 12 * sizeof(float)));

	float dS = 1.0f / horizonNum;
	float dT = 1.0f / verticalNum;

	for (int i = 0; i < horizonNum; ++i) //S
	{
		float s0 = i * dS;
		float s1 = (1 + i) * dS;
		for (int j = 0; j < verticalNum; ++j) //T
		{
			float t0 = j * dT;
			float t1 = (1 + j) * dT;
			int meshIndex = j * horizonNum + i;
			(*ppTexCoor)[meshIndex * 12 + 0] = s0;
			(*ppTexCoor)[meshIndex * 12 + 1] = t0;

			(*ppTexCoor)[meshIndex * 12 + 2] = s0;
			(*ppTexCoor)[meshIndex * 12 + 3] = t1;


			(*ppTexCoor)[meshIndex * 12 + 4] = s1;
			(*ppTexCoor)[meshIndex * 12 + 5] = t0;

			(*ppTexCoor)[meshIndex * 12 + 6] = s1;
			(*ppTexCoor)[meshIndex * 12 + 7] = t0;


			(*ppTexCoor)[meshIndex * 12 + 8] = s0;
			(*ppTexCoor)[meshIndex * 12 + 9] = t1;

			(*ppTexCoor)[meshIndex * 12 + 10] = s1;
			(*ppTexCoor)[meshIndex * 12 + 11] = t1;

			// vertex coordinate
			(*ppVertices)[meshIndex * 18 + 0] = 2 * s0 - 1;
			(*ppVertices)[meshIndex * 18 + 1] = 1 - 2 * t0;
			(*ppVertices)[meshIndex * 18 + 2] = 0;

			(*ppVertices)[meshIndex * 18 + 3] = 2 * s0 - 1;
			(*ppVertices)[meshIndex * 18 + 4] = 1 - 2 * t1;
			(*ppVertices)[meshIndex * 18 + 5] = 0;

			(*ppVertices)[meshIndex * 18 + 6] = 2 * s1 - 1;
			(*ppVertices)[meshIndex * 18 + 7] = 1 - 2 * t0;
			(*ppVertices)[meshIndex * 18 + 8] = 0;

			(*ppVertices)[meshIndex * 18 + 9] = 2 * s1 - 1;
			(*ppVertices)[meshIndex * 18 + 10] = 1 - 2 * t0;
			(*ppVertices)[meshIndex * 18 + 11] = 0;

			(*ppVertices)[meshIndex * 18 + 12] = 2 * s0 - 1;
			(*ppVertices)[meshIndex * 18 + 13] = 1 - 2 * t1;
			(*ppVertices)[meshIndex * 18 + 14] = 0;

			(*ppVertices)[meshIndex * 18 + 15] = 2 * s1 - 1;
			(*ppVertices)[meshIndex * 18 + 16] = 1 - 2 * t1;
			(*ppVertices)[meshIndex * 18 + 17] = 0;

		}
	}

	memcpy(m_pStatusTexCoords, (*ppTexCoor), verticalNum * horizonNum * 12 * sizeof(float));
}

在圆的范围内控制网格结点的偏移,圆形之外的区域不进行偏移形变(不受影响),圆内的区域的像素则是越靠近圆心移动位移相对越大。

//prePoint 圆心,radius 半径
void GLRender::UpdateVertexMeshWithLinear(PointF prePoint, PointF curPoint, float radius, float *pVertices)
{

	int pointNum = m_MeshNum * 6;
	float textureWidth = m_RenderImg.width;
	float textureHeight = m_RenderImg.height;

	// 转化为图片坐标
	float imgRadius = radius * textureWidth;
	PointF imgSize = {textureWidth, textureHeight};
	PointF imgPrePoint = prePoint * imgSize;
	PointF imgCurPoint = curPoint * imgSize;
	PointF texPoint;
	for (int i = 0; i < pointNum; ++i)
	{
		texPoint.x = m_TexCoords[i * 2 + 0];
		texPoint.y = m_TexCoords[i * 2 + 1];

		PointF imgTexPoint = texPoint * imgSize;
		float r = distance(imgTexPoint, imgPrePoint);
		//判断是否在圆的范围内
		if (r < imgRadius)
		{
            //越靠近圆心偏移越大
			float alpha = 1.0f - r / imgRadius;
			//做个平滑
			alpha = smoothstep(0.f, 1.f, alpha);

			//移动方向
			PointF dVec = (imgCurPoint - imgPrePoint) * pow(alpha, 2.0f); 
            //乘以一个系数,不然偏移太大了
			dVec = dVec * 0.08f;
			//移动后的网格结点
			PointF newImgTexPoint = imgTexPoint + dVec;
			//归一化
			newImgTexPoint = newImgTexPoint / imgSize;

			//更新对应的纹理坐标和顶点坐标
			m_TexCoords[i * 2 + 0] = newImgTexPoint.x;
			m_TexCoords[i * 2 + 1] = newImgTexPoint.y;

			pVertices[i * 3 + 0] = 2 * newImgTexPoint.x - 1;
			pVertices[i * 3 + 1] = 1 - 2 * newImgTexPoint.y;
		}
	}

}

纹理贴图使用的 shader,一个常规的纹理采样。

const char vShaderStr[] =
        "#version 300 es                            \n"
        "layout(location = 0) in vec4 a_position;   \n"
        "layout(location = 1) in vec2 a_texCoord;   \n"
        "out vec2 v_texCoord;                       \n"
        "uniform mat4 u_MVPMatrix;                  \n"
        "void main()                                \n"
        "{                                          \n"
        "   gl_Position = u_MVPMatrix * a_position; \n"
        "   v_texCoord = a_texCoord;                \n"
        "}                                          \n";

const char fShaderStr[] =
        "#version 300 es                            \n"
        "precision mediump float;                   \n"
        "in vec2 v_texCoord;                        \n"
        "layout(location = 0) out vec4 outColor;    \n"
        "uniform sampler2D s_TextureMap;            \n"
        "void main()                                \n"
        "{                                          \n"
        "    outColor = texture(s_TextureMap, v_texCoord);\n"
        "}";

推荐参考项目:github.com/githubhaoha…