当前位置: 首页 > news >正文

Android OpenGL ES 实现 3D 阿凡达效果

3D 效果的壁纸

本文实现的效果

偶然间,看到技术交流群里的一位同学在做类似于上图所示的 3D 效果壁纸,乍一看效果确实挺惊艳的。当时看到素材之后,马上就萌生了一个想法:利用 OpenGL 做一个能与之媲美的 3D 效果。

拿到素材之后,就开始撸代码,想着就是简单的图像绘制加上矩阵变换嘛,花半个小时搞定它,谁曾想故事远没那么简单。另外,这里特别感谢交流群里的 @1234 同学,提供了本文所需的素材。

1

3D 效果实现原理

毫无疑问,这种 3D 效果选择使用 OpenGL 实现是再合适不过了,当然 Vulkan 也挺香的。通过观察上图 3D 壁纸的效果,罗列一下我们可能要用到的技术点:

  • 纹理映射

  • 图像坐标变换,坐标系统矩阵变换实现图像的位移和缩放;

  • 监听手机传感器数据,利用传感器数据控制图像位移。

绘制原理图

基于 3D 壁纸的效果画出以上原理图,每一次渲染包含 3 次小的绘制,即分别绘制背景层、人像层和外层。

手机晃动时,通过 Java 层 API 获取重力传感器数据(不是加速度传感器),控制 3 张图像在平面四个方向的偏移,从背景层到外层偏移程度依次增大,从而给人一种 3D 的层次感。

Android 设备重力传感器数据的获取方法:

@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 效果。

使用 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 测试。

用于 Alpha 测试的着色器脚本。

//顶点着色器
#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;//丢弃透明度比较低的片元
}




2

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 阿凡达效果如下图所示。

手机晃动状态下的效果

手机静止状态下的效果

实现代码路径见阅读原文末。

推荐阅读:

  • 【数字视频技术介绍】| 视频帧类型(I 帧、P 帧、B 帧)

  • 移动端技术交流喊你入群啦~~~

  • 推荐几个堪称教科书级别的 Android 音视频入门项目

  • 一加8 Pro如何优化120Hz屏幕

  • 没想到,快手成了“生产力”

  • Shader 优化 | OpenGL 绘制网格效果

  • Android OpenGL 学习专辑

觉得不错,点个在看呗~

相关文章:

  • 音视频开发入门必备之基础知识
  • 6/6 音视频技术大咖在线直播,教你开发者硬核个人成长指南
  • 关于多线程,你必须知道的那些玩意儿
  • Android Camera2 实现高帧率预览录制(附源码)
  • 自定义相机中如何实现二维码扫描功能
  • 渐变过渡的相册(shader)
  • 【C++11新特性】 C++11 智能指针之shared_ptr
  • 【C++11新特性】 C++11智能指针之weak_ptr
  • 5 个 IDEA 必备插件,让效率成为习惯
  • 【C++11新特性】 C++11智能指针之 unique_ptr
  • 关于JVM,你必须知道的那些玩意儿
  • OpenGL ES 实现动态(水波纹)涟漪效果
  • 盘点Android常用Hook技术
  • 推荐一款强大的 Android OpenGL ES 调试工具
  • MediaCodec 解码后数据对齐导致的绿边问题
  • 30秒的PHP代码片段(1)数组 - Array
  • Java,console输出实时的转向GUI textbox
  • JavaScript HTML DOM
  • js作用域和this的理解
  • MobX
  • MySQL的数据类型
  • session共享问题解决方案
  • sublime配置文件
  • VuePress 静态网站生成
  • vue--为什么data属性必须是一个函数
  • windows下mongoDB的环境配置
  • 理解在java “”i=i++;”所发生的事情
  • 如何使用 JavaScript 解析 URL
  • 实习面试笔记
  • Hibernate主键生成策略及选择
  • 支付宝花15年解决的这个问题,顶得上做出十个支付宝 ...
  • ​io --- 处理流的核心工具​
  • ​人工智能书单(数学基础篇)
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (02)vite环境变量配置
  • (1)Map集合 (2)异常机制 (3)File类 (4)I/O流
  • (10)ATF MMU转换表
  • (11)MATLAB PCA+SVM 人脸识别
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (笔试题)合法字符串
  • (附源码)ssm教师工作量核算统计系统 毕业设计 162307
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (论文阅读30/100)Convolutional Pose Machines
  • (译)2019年前端性能优化清单 — 下篇
  • (转)VC++中ondraw在什么时候调用的
  • (轉貼) 寄發紅帖基本原則(教育部禮儀司頒布) (雜項)
  • .NET 4.0中使用内存映射文件实现进程通讯
  • .net core 微服务_.NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端
  • .net core使用RPC方式进行高效的HTTP服务访问
  • .net MVC中使用angularJs刷新页面数据列表
  • .Net Remoting常用部署结构
  • .NET 线程 Thread 进程 Process、线程池 pool、Invoke、begininvoke、异步回调
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • /*在DataTable中更新、删除数据*/