該原創文章首發於微信公衆號:字節流動java
偶然間,看到技術交流羣裏的一位同窗在作相似於上圖所示的 3D 效果壁紙,乍一看效果確實挺驚豔的。當時看到素材以後,立刻就萌生了一個想法:利用 OpenGL 作一個能與之媲美的 3D 效果。c++
拿到素材以後,就開始擼代碼,想着就是簡單的圖像繪製加上矩陣變換嘛,花半個小時搞定它,誰曾想故事遠沒那麼簡單。另外,這裏特別感謝交流羣裏的 @1234 同窗,提供了本文所需的素材。微信
毫無疑問,這種 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 效果。
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 , 獲取音視頻開發視頻教程