OpenGL ES 實現頭部形變和頭部晃動效果

位元組流動發表於2020-03-30

小姐姐說,我頭都被你氣大了,怎麼辦?

該原創文章首發於微信公眾號:位元組流動

舊文中我們利用 OpenGL 給小姐姐實現了瘦身、大長腿效果以及瘦臉大眼效果,小姐姐苦笑道:我頭都被你氣大了,怎麼辦?

怎麼辦?對於一個直男癌晚期的碼農來說,這都不是事兒。

大頭小頭效果

大頭小頭效果

舊文中我們知道,利用 OpenGL 紋理對映(紋理貼圖)的基本原理,可以很輕易的實現對影像指定的區域進行拉伸和縮放。

典型的紋理對映著色器。

//頂點著色器
#version 300 es
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec2 a_texCoord;
uniform mat4 u_MVPMatrix;
out vec2 v_texCoord;
void main()
{
    gl_Position = u_MVPMatrix * a_position;
    v_texCoord = a_texCoord;
}

//片段著色器
#version 300 es
precision highp float;
layout(location = 0) out vec4 outColor;
in vec2 v_texCoord;
uniform sampler2D s_TextureMap;
void main() {
    outColor = texture(s_TextureMap, v_texCoord);
}
複製程式碼

舊文中,紋理對映都是發生在規則的矩形區域,如瘦身大長腿效果,而本文的大頭小頭效果實際上是對不規則的臉部區域進行縮放。這時就不能按照規則的矩形來劃分網格,原因有兩個:(1)因為我們只想形變發生在頭部區域,而規則的矩形網格會導致影像背景發生畸變;(2)通過規則的矩形網格難以控制對頭部(不規則)區域的形變程度

紋理座標系,輻射狀的網格結構

為了防止背景發生嚴重的畸變,我們設計如上圖所示輻射狀的網格結構。對頭部區域進行形變就需要知道頭部區域的關鍵點,頭部區域的關鍵點可以通過 AI 演算法來獲得。這裡為了展示方便,將頭部區域的關鍵點簡化為 9 個,其中 8 個點位於頭部邊緣,一個點位於頭部中心位置。

直線 x=1y=1 和紋理座標軸連成了一個矩形,每個頭部邊緣的關鍵點和頭部中心點確定一條直線,該直線會與矩形的邊存在交點,我們用這些交點和頭部關鍵點來構建這個呈輻射狀的網格。

紋理座標系中計算交點

如上圖所示,每個頭部邊緣關鍵點和頭部中心點確定一條直線,這條直線可以用二元一次方程來表示,它與上述矩形邊的交點,可以通過求解二元一次方程得出。

通過關鍵點計算出交點的函式如下(inputPoint 表示頭部邊緣關鍵點,centerPoint 表示頭部中心點,DotProduct 函式表示計算兩個向量的點積):

vec2 BigHeadSample::CalculateIntersection(vec2 inputPoint, vec2 centerPoint) {
	vec2 outputPoint;
	if(inputPoint.x == centerPoint.x) //直線與 y 軸平行
	{
		vec2 pointA(inputPoint.x, 0);
		vec2 pointB(inputPoint.x, 1);

		float dA = distance(inputPoint, pointA);
		float dB = distance(inputPoint, pointB);
		outputPoint = dA > dB ? pointB : pointA;
		return outputPoint;
	}

	if(inputPoint.y == centerPoint.y) //直線與 x 軸平行
	{
		vec2 pointA(0, inputPoint.y);
		vec2 pointB(1, inputPoint.y);

		float dA = distance(inputPoint, pointA);
		float dB = distance(inputPoint, pointB);
		outputPoint = dA > dB ? pointB : pointA;
		return outputPoint;
	}

	// y = a*x + c
	float a=0, c=0;

	a = (inputPoint.y - centerPoint.y) / (inputPoint.x - centerPoint.x);

	c = inputPoint.y - a * inputPoint.x;

	//x=0, x=1, y=0, y=1 四條線交點

	//x=0
	vec2 point_0(0, c);
	float d0 = DotProduct((centerPoint - inputPoint),(centerPoint - point_0));

	if(c >= 0 && c <= 1 && d0 > 0)
		outputPoint = point_0;

	//x=1
	vec2 point_1(1, a + c);
	float d1 = DotProduct((centerPoint - inputPoint),(centerPoint - point_1));

	if((a + c) >= 0 && (a + c) <= 1 && d1 > 0)
		outputPoint = point_1;

	//y=0
	vec2 point_2(-c / a, 0);
	float d2 = DotProduct((centerPoint - inputPoint),(centerPoint - point_2));

	if((-c / a) >= 0 && (-c / a) <= 1 && d2 > 0)
		outputPoint = point_2;

	//y=1
	vec2 point_3((1-c) / a, 1);
	float d3 = DotProduct((centerPoint - inputPoint),(centerPoint - point_3));

	if(((1-c) / a) >= 0 && ((1-c) / a) <= 1 && d3 > 0)
		outputPoint = point_3;

	return outputPoint;
}
複製程式碼

在紋理座標系上構建好輻射狀的網格之後,需要進行座標系變換,即將紋理座標系轉換為渲染座標系(螢幕座標系),得到紋理座標所對應的頂點座標。

紋理將座標系轉換為渲染座標系(螢幕座標系)的對應關係
(x,y)->(2*x-1, 1-2*y)

複製程式碼

另外,控制頭部變大和變小實際上是,通過控制頭部邊緣關鍵點對應頂點座標的相對位置來實現的,當頭部邊緣關鍵點對應的頂點座標靠近頭部中心點時,頭部變小,遠離頭部中心點時,反之變大。

頂點座標靠近頭部中心點

如上圖所示,頭部邊緣關鍵點對應的頂點座標靠近頭部中心點,在計算上可以通過點與向量相加來實現。點與向量相加的幾何意義是點按照向量的方向移動一定的距離,該向量可以通過頭部中心點座標減去邊緣關鍵點座標得出。

移動邊緣關鍵點的函式。

//input 為邊緣關鍵點,centerPoint 為頭部中心點,level 控制移動的距離 
vec2 BigHeadSample::WarpKeyPoint(vec2 input, vec2 centerPoint, float level) {
	vec2 output;
	vec2 direct_vec = centerPoint - input;
	output = input + level * direct_vec * 0.3f;
	return output;
}
複製程式碼

更新移動後的關鍵點座標,繪製影像。

//設定視口
glViewport(0, 0, screenW, screenH);

m_FrameIndex ++;

//變換矩陣
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);

//強度
float ratio = (m_FrameIndex % 100) * 1.0f / 100;
ratio = (m_FrameIndex / 100) % 2 == 1 ? (1 - ratio) : ratio;

//計算新的網格
CalculateMesh(ratio - 0.5f);

//更新頂點陣列
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_Vertices), m_Vertices);

//繪製影像
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawArrays(GL_TRIANGLES, 0, TRIANGLE_COUNT * 3);
複製程式碼

頭部晃動效果

帶網格的頭部晃動效果

那麼如何實現頭部晃動的效果呢?答案還是控制頭部關鍵點的位置。簡而言之就是,控制頭部所有關鍵點統一按照某一圓的軌跡進行移動,我們這裡指的頭部關鍵點是在螢幕座標系中紋理座標所對應的點。

實現關鍵點按照某一圓的軌跡進行移動的函式(input 為頭部關鍵點,rotaryAngle 為轉動角度)。

vec2 RotaryHeadSample::RotaryKeyPoint(vec2 input, float rotaryAngle) {
	return input + vec2(cos(rotaryAngle), sin(rotaryAngle)) * 0.02f; // 0.02f 表示圓的半徑
}
複製程式碼

更新移動後的關鍵點座標,繪製影像。

//設定視口
glViewport(0, 0, screenW, screenH);

m_FrameIndex ++;

//變換矩陣
UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, (float)screenW / screenH);

float ratio = (m_FrameIndex % 100) * 1.0f / 100;

//計算新的網格
CalculateMesh(static_cast<float>(ratio * 2 * MATH_PI));

//更新頂點陣列
glBindBuffer(GL_ARRAY_BUFFER, m_VboIds[0]);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(m_Vertices), m_Vertices);

//繪製影像
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);
glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureId);
glUniform1i(m_SamplerLoc, 0);
glDrawArrays(GL_TRIANGLES, 0, TRIANGLE_COUNT * 3);
複製程式碼

頭部晃動效果

實現程式碼路徑: Android_OpenGLES_3_0

聯絡與交流

OpenGL ES 實現頭部形變和頭部晃動效果

相關文章