OpenGL ES 實現 3D 阿凡達效果

該原創文章首發於微信公衆號:字節流動java

3D 效果的壁紙

偶然間,看到技術交流羣裏的一位同窗在作相似於上圖所示的 3D 效果壁紙,乍一看效果確實挺驚豔的。當時看到素材以後,立刻就萌生了一個想法:利用 OpenGL 作一個能與之媲美的 3D 效果。c++

拿到素材以後,就開始擼代碼,想着就是簡單的圖像繪製加上矩陣變換嘛,花半個小時搞定它,誰曾想故事遠沒那麼簡單。另外,這裏特別感謝交流羣裏的 @1234 同窗,提供了本文所需的素材。微信

3D 效果實現原理

毫無疑問,這種 3D 效果選擇使用 OpenGL 實現是再合適不過了,固然 Vulkan 也挺香的。經過觀察上圖 3D 壁紙的效果,羅列一下咱們可能要用到的技術點:markdown

  • 紋理映射,繪製圖像;
  • 圖像座標變換,座標系統矩陣變換實現圖像的位移和縮放;
  • 監聽手機傳感器數據,利用傳感器數據控制圖像位移。

繪製原理圖

基於 3D 壁紙的效果畫出以上原理圖,每一次渲染包含 3 次小的繪製,即分別繪製背景層、人像層和外層。手機晃動時,經過 Java 層 API 獲取重力傳感器數據(不是加速度傳感器),控制 3 張圖像在平面四個方向的偏移,從背景層到外層偏移程度依次增大,從而給人一種 3D 的層次感。ide

Android 設備重力傳感器數據的獲取方法:oop

@Override
protected void onResume() {
    super.onResume();
    mSensorManager.registerListener(this,
            mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY),
            SensorManager.SENSOR_DELAY_FASTEST);
}

@Override
protected void onPause() {
    super.onPause();
    mSensorManager.unregisterListener(this);
}

@Override
public void onSensorChanged(SensorEvent event) {
    switch (event.sensor.getType()) {
        case Sensor.TYPE_GRAVITY:
            Log.d(TAG, "onSensorChanged() called with TYPE_GRAVITY: [x,y,z] = [" + event.values[0] + ", " + event.values[1] + ", " + event.values[2] + "]");
            if(mSampleSelectedIndex + SAMPLE_TYPE == SAMPLE_TYPE_KEY_AVATAR)
            {
                mGLRender.setGravityXY(event.values[0], event.values[1]);
            }
            break;
    }

}
複製代碼

另外,經過觀察效果圖還發現,3 張圖像還有周期性的縮放,而且背景層、外層和人像層的縮放程度大小相反,這種作法也是爲了強化 3D 效果。post

使用 Native 層的變換矩陣,用於控制圖像位移和縮放。測試

/** * * @param mvpMatrix * @param angleX 繞X軸旋轉度數 * @param angleY 繞Y軸旋轉度數 * @param transX 沿X軸位移大小 * @param transY 沿Y軸位移大小 * @param ratio 寬高比 */
void AvatarSample::UpdateMVPMatrix(glm::mat4 &mvpMatrix, int angleX, int angleY, float transX, float transY, float ratio) {
	LOGCATE("AvatarSample::UpdateMVPMatrix angleX = %d, angleY = %d, ratio = %f", angleX, angleY, ratio);
	angleX = angleX % 360;
	angleY = angleY % 360;

	//轉化爲弧度角
	float radiansX = static_cast<float>(MATH_PI / 180.0f * angleX);
	float radiansY = static_cast<float>(MATH_PI / 180.0f * angleY);


	// Projection matrix
	glm::mat4 Projection = glm::ortho(-1.0f, 1.0f, -1.0f, 1.0f, 0.1f, 100.0f);
	//glm::mat4 Projection = glm::frustum(-ratio, ratio, -1.0f, 1.0f, 4.0f, 100.0f);
	//glm::mat4 Projection = glm::perspective(45.0f,ratio, 0.1f,100.f);

	// View matrix
	glm::mat4 View = glm::lookAt(
			glm::vec3(0, 0, 4), // Camera is at (0,0,1), in World Space
			glm::vec3(0, 0, 0), // and looks at the origin
			glm::vec3(0, 1, 0)  // Head is up (set to 0,-1,0 to look upside-down)
	);

	// Model matrix
	glm::mat4 Model = glm::mat4(1.0f);
	Model = glm::scale(Model, glm::vec3(m_ScaleX, m_ScaleY, 1.0f));//m_ScaleX, m_ScaleY 用於控制 x,y 方向上的縮放
	Model = glm::rotate(Model, radiansX, glm::vec3(1.0f, 0.0f, 0.0f));
	Model = glm::rotate(Model, radiansY, glm::vec3(0.0f, 1.0f, 0.0f));
	Model = glm::translate(Model, glm::vec3(transX, transY, 0.0f));

	mvpMatrix = Projection * View * Model;

}
複製代碼

素材圖裏的人像層和外層是部分區域透明的 PNG 圖,而背景層是每一個像素透明度均爲最大值的 JPG 圖。因此,在繪製 3 張圖時,要先繪製背景層,而後依次是人像層、外層,爲了防止遮擋,在繪製人像層、外層時須要利用片斷着色器來丟棄透明度比較低的片元,這種操做俗稱 alpha 測試。ui

用於 Alpha 測試的着色器腳本。this

//頂點着色器
#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;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
void main() {
    outColor = texture(s_TextureMap, v_texCoord);
    if (outColor.a < 0.6) discard;//丟棄透明度比較低的片元
}
複製代碼

3D 效果實現

基於上節原理和知識點準備,咱們使用下面的代碼繪製 3D 效果。

void AvatarSample::Draw(int screenW, int screenH) {
	LOGCATE("AvatarSample::Draw()");
	if(m_ProgramObj == GL_NONE) return;
	float dScaleLevel = m_FrameIndex % 200 * 1.0f / 1000 + 0.0001f;
	float scaleLevel = 1.0;

	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// Use the program object
	glUseProgram(m_ProgramObj);
	glBindVertexArray(m_VaoId);


	//1. 背景層的繪製
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
	glUniform1i(m_SamplerLoc, 0);

	//縮放控制
	scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200));
	scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
	m_ScaleY = m_ScaleX = scaleLevel + 0.4f;

	//設置變換矩陣 m_TransX m_TransY 爲 x,y 方向的重力傳感器數據
	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX / 2, m_TransY / 2, (float)screenW / screenH);
	glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
    
    //繪製
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);


    //2. 人像層的繪製
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, m_TextureIds[1]);
	glUniform1i(m_SamplerLoc, 1);

    //縮放控制 pow(-1, m_FrameIndex / 200 + 1) 控制人像層的縮放大小跟背景層和外層相反
	scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200 + 1));
	scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
	m_ScaleY = m_ScaleX = scaleLevel + 0.4f;

	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX * 1.2f, m_TransY * 1.2f, (float)screenW / screenH);
	glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);


	//3. 外層的繪製
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, m_TextureIds[2]);
	glUniform1i(m_SamplerLoc, 2);

	scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200));
	scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
	m_ScaleY = m_ScaleX = scaleLevel + 0.8f;

	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX * 2.5f, m_TransY * 2.5f, (float)screenW / screenH);
	glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);


	m_FrameIndex ++;

}

複製代碼

繪製效果以下圖所示,咱們指望的縮放和位移基本上實現了,可是仔細對比原效果圖,很容易發現一些問題:最外層的白斑缺乏一種模糊的過分,而且白點的亮度也不夠。

第一版效果圖

說到模糊效果,以前在介紹相機濾鏡那篇文章裏說過一種最簡單的疊加偏移模糊,咱們能夠在繪製外層圖像時,使用這種模糊效果。另外,參考效果圖後,爲了使白斑變的更大更亮,咱們還須要用到混合光照

繪製外層圖像的片斷着色器以下,着色器中,咱們經過放寬 alpha 值過濾範圍,使白斑變的更大,同時將輸出顏色疊加必定的強度值,使白斑變的更亮。

//繪製外層圖像的片斷着色器
#version 300 es
precision highp float;
layout(location = 0) out vec4 outColor;
in vec2 v_texCoord;
uniform sampler2D s_TextureMap;
void main() {
    vec4 sample0, sample1, sample2, sample3;
    float blurStep = 0.2;
    float step = blurStep / 100.0;
    sample0 = texture(s_TextureMap, vec2(v_texCoord.x - step, v_texCoord.y - step));
    sample1 = texture(s_TextureMap, vec2(v_texCoord.x + step, v_texCoord.y + step));
    sample2 = texture(s_TextureMap, vec2(v_texCoord.x + step, v_texCoord.y - step));
    sample3 = texture(s_TextureMap, vec2(v_texCoord.x - step, v_texCoord.y + step));
    outColor = (sample0 + sample1 + sample2 + sample3) / 4.0;
    if (outColor.a > 0.03) //放寬 alpha 值過濾範圍,使白斑變的更大
    {
        outColor += vec4(0.1, 0.1, 0.1, 0.0); //疊加一些強度,使白斑變的更亮
    }
    else
    {
        discard;
    }
}
複製代碼

修改外層圖像的繪製邏輯,添加混合。

//開啓混合
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_COLOR, GL_ONE_MINUS_SRC_ALPHA);

//使用新的着色器程序
glUseProgram(m_BlurProgramObj);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[2]);
GLUtils::setFloat(m_BlurProgramObj, "s_TextureMap", 0);

scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200));
scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
m_ScaleY = m_ScaleX = scaleLevel + 0.8f;

UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX * 2.5f, m_TransY * 2.5f, (float)screenW / screenH);
GLUtils::setMat4(m_BlurProgramObj, "u_MVPMatrix", m_MVPMatrix);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);

//關閉混合
glDisable(GL_BLEND);

複製代碼

添加模糊和混合以後的繪製結果以下,看着效果符合預期,頓時有那麼一點點成就感。 第二版效果圖

正當我覺得故事圓滿結束的時候,旁邊的小夥伴不屑地看了看我作的效果,並對比了下原效果圖,而後一臉壞笑的說:「你看,人家作的背景還有形變啊!」,我內心頓時一句「臥槽」。

而後我仔細觀察了下原效果圖的背景形變,想起來以前在介紹 EGL 那篇文章裏作過一種簡單的正餘弦形變,形變效果以下圖所示。 旋轉形變

作背景形變用到的片斷着色器,須要傳入圖像分辨率、控制形變的標誌位以及旋轉角度,其中旋轉角度須要與重力傳感器數據綁定,實現晃動手機出現相關的動態背景形變。

//片斷着色器
#version 300 es
precision highp float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
uniform vec2 u_texSize;//圖像分辨率
uniform float u_needRotate;//判斷是否須要作形變
uniform float u_rotateAngle;//經過旋轉角度控制形變的程度

vec2 rotate(float radius, float angle, vec2 texSize, vec2 texCoord) {
    vec2 newTexCoord = texCoord;
    vec2 center = vec2(texSize.x / 2.0, texSize.y / 2.0);
    vec2 tc = texCoord * texSize;
    tc -= center;
    float dist = length(tc);
    if (dist < radius) {
        float percent = (radius - dist) / radius;
        float theta = percent * percent * angle * 8.0;
        float s = sin(theta);
        float c = cos(theta);
        tc = vec2(dot(tc, vec2(c, -s)), dot(tc, vec2(s, c)));
        tc += center;

        newTexCoord = tc / texSize;
    }
    return newTexCoord;
}
void main() {
    vec2 texCoord = v_texCoord;

    if(u_needRotate > 0.0)
    {
        texCoord = rotate(0.5, u_rotateAngle, u_texSize, v_texCoord);
    }

    outColor = texture(s_TextureMap, texCoord);
    if (outColor.a < 0.6) discard;
}
複製代碼

基於以上的着色器,咱們單獨繪製背景圖,令形變的旋轉角度與重力傳感器數據綁定,效果以下圖所示。

背景形變圖

綜合了以上場景,咱們最終的繪製邏輯以下:

void AvatarSample::Draw(int screenW, int screenH) {
	LOGCATE("AvatarSample::Draw()");

	if(m_ProgramObj == GL_NONE) return;
	float dScaleLevel = m_FrameIndex % 200 * 1.0f / 1000 + 0.0001f;
	float scaleLevel = 1.0;

	glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
	glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glUseProgram(m_ProgramObj);
	glBindVertexArray(m_VaoId);

    //1. 背景層的繪製
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
	glUniform1i(m_SamplerLoc, 0);
	scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200));
	scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
	m_ScaleY = m_ScaleX = scaleLevel + 0.4f;
	GLUtils::setVec2(m_ProgramObj, "u_texSize", glm::vec2(m_RenderImages[0].width, m_RenderImages[0].height));
	GLUtils::setFloat(m_ProgramObj, "u_needRotate", 1.0f); // u_needRotate == 1 開啓形變
	GLUtils::setFloat(m_ProgramObj, "u_rotateAngle", m_TransX * 1.5f);
	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX / 2, m_TransY / 2, (float)screenW / screenH);
	glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);

    //2. 人像層的繪製
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, m_TextureIds[1]);
	glUniform1i(m_SamplerLoc, 1);
	scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200 + 1));
	scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
	m_ScaleY = m_ScaleX = scaleLevel + 0.4f;
	LOGCATE("AvatarSample::Draw() scaleLevel=%f", scaleLevel);
	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX * 1.2f, m_TransY * 1.2f, (float)screenW / screenH);
	GLUtils::setVec2(m_ProgramObj, "u_texSize", glm::vec2(m_RenderImages[0].width, m_RenderImages[0].height));
	GLUtils::setFloat(m_ProgramObj, "u_needRotate", 0.0f);// u_needRotate == 0 關閉形變
	GLUtils::setFloat(m_ProgramObj, "u_rotateAngle", m_TransX / 20);
	glUniformMatrix4fv(m_MVPMatLoc, 1, GL_FALSE, &m_MVPMatrix[0][0]);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);

    //3. 外層的繪製
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_COLOR, GL_ONE_MINUS_SRC_ALPHA);
    //切換另一個着色器程序
	glUseProgram(m_BlurProgramObj);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_TextureIds[2]);
	GLUtils::setFloat(m_BlurProgramObj, "s_TextureMap", 0);
	scaleLevel = static_cast<float>(1.0f + dScaleLevel * pow(-1, m_FrameIndex / 200));
	scaleLevel = scaleLevel < 1.0 ? scaleLevel + 0.2f : scaleLevel;
	m_ScaleY = m_ScaleX = scaleLevel + 0.8f;
	UpdateMVPMatrix(m_MVPMatrix, m_AngleX, m_AngleY, m_TransX * 2.5f, m_TransY * 2.5f, (float)screenW / screenH);
	GLUtils::setMat4(m_BlurProgramObj, "u_MVPMatrix", m_MVPMatrix);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);

	glDisable(GL_BLEND);
	m_FrameIndex ++;

}
複製代碼

最終的 3D 阿凡達效果以下圖所示。

手機晃動狀態下的效果

手機靜止狀態下的效果

技術疑問解答

技術交流或獲取源碼能夠添加個人微信:Byte-Flow , 獲取音視頻開發視頻教程

相關文章
相關標籤/搜索