OpenGL ES在做普通应用方面3D使用的不多,但有时候实现一些有趣的功能也是蛮不错的。画立方体的的demo网上已经很多了,这次我们就实现一个随手指旋转的立方体,这个demo基本可以了解各个坐标系转换矩阵的使用了。

先看一下最终效果:

android实现3D瓦片图 android绘制3d图形_Opengl


话不多说,直接上代码了。

EGL的配置

EGL的配置也就是常规配置了,但是需要注意的一点是:为了使立方体看起来更加真实,需要开启深度测试,需要在egl的环境中加入深度测试的配置。不然就算启用了深度测试也会没用。

//通过属性去筛选合适的配置
    const EGLint attibutes[] = {
            EGL_BUFFER_SIZE, 32,
            EGL_ALPHA_SIZE, 8,
            EGL_BLUE_SIZE, 8,
            EGL_GREEN_SIZE, 8,
            EGL_RED_SIZE, 8,
            EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, //指定渲染api版本 2
            EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
            EGL_DEPTH_SIZE, 24, //添加这段来请求深度缓冲区
            EGL_NONE
    };

绘制一个立方体

首先使用OpenGL绘制一个立方体十分简单,一个面两个三角形,绘制十二个三角形就可以了。一个三角形3个点,也就是需要36个点。其实立方体很多点是重复的,我们完全可以用下标的形式去画点,但是这篇文章我没有用下标,使用了36个点来直接画。

float vertices[] = {
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
            0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, 0.0f,

            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
            0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
            -0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f,

            -0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
            -0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
            -0.5f, 0.5f, 0.5f, 1.0f, 0.0f,

            0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
            0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
            0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f,

            -0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
            0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
            0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
            0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
            -0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
            -0.5f, -0.5f, -0.5f, 0.0f, 1.0f,

            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
            0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
            -0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
            -0.5f, 0.5f, -0.5f, 0.0f, 1.0f
    };
    glDrawArrays(GL_TRIANGLES, 0, 36);

这个立方体可以想象一下,就是边长为1.0的立方体了,中心在原点。

矩阵变换

我们也还是按照几种常用矩阵变换的套路来,我们还是使用模型,视图,投影矩阵来变换坐标。

#version 300 es
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;


void main(){
   gl_Position = projection*view*model*vec4(aPos, 1.0f);
   TexCoord=vec2(aTexCoord.x,1.0-aTexCoord.y);
}
1、不同向量的旋转

首先我们在只考虑在model中加入旋转。想象一下手指在屏幕上滑动是有一个方向向量的,加入我们在屏幕上这样滑动

android实现3D瓦片图 android绘制3d图形_旋转立方体_02


那么立方体的旋转轴就是垂直于手指滑动方向的一个向量,而这个向量的求取方式,我们使用这个向量于Z轴的向量进行叉乘来取得。还要注意一点,因为我们取得屏幕坐标系和OpenGL的坐标系又差别,所以需要一定的变化来求得正确的旋转轴。

if (x > 0 && y > 0) {
        y = -y;
    } else if (x <= 0 && y > 0) {
        x = -x;
        degree = -degree;
    } else if (x > 0 && y < 0) {
        y = -y;
    } else if (x <= 0 && y < 0) {
        y = -y;
    }
   LOGE("tedu %f %f %f" ,x,y,degree);

    if (degree != 0) {
        glm::vec3 cross = glm::cross(glm::vec3(0.0f, 0.0f, 1.0), glm::vec3(x, y, 0.0f));
        glm::mat4 tmpMat = glm::mat4(1.0f);
        tmpMat = glm::rotate(tmpMat, degree, cross);
        tmpMat *= model;
        model = tmpMat;
    }

简单分析一下,我们每次求得对应的向量后,生成一个新的选择矩阵,然后右乘原来的矩阵,注意不能交换顺序,然后求得最终的矩阵并赋值了model。通过状态的叠加,最终将会获得最终的效果。

2、双指的缩放

这里就不得不提一下对于双指的判定了。我们的逻辑是:

  • 单指的时候不进行缩放,进行旋转
  • 双指的时候进行缩放,不进行旋转
  • 双指以上情况直接不处理,或者有双指变为单指的情况也不处理
    逻辑理顺之后,我们只需要加入判断的bool变量即可,注意在抬起手指,以及单指切换双指的时候需要清空旋转的值,看一下逻辑。
cube.setOnTouchListener { v, event ->


            when (event.actionMasked) {
                MotionEvent.ACTION_DOWN -> {
                 //单指触摸情况
                    lastX = event.getX()
                    lastY = event.getY()
                    canRotate = true
                    canScale = false

                }
                MotionEvent.ACTION_POINTER_DOWN -> {
                    //双指触摸
                    canRotate = false
                    canScale = true
                    if (event.pointerCount > 2) {
                        canScale = false
                        return@setOnTouchListener false
                    }
                    val index0 = event.getPointerId(0)
                    val index1 = event.getPointerId(1)
                    lastX = event.getX(index0)
                    lastY = event.getY(index0)
                    last1X = event.getX(index1)
                    last1Y = event.getY(index1)
                    controler.rotate(0.0f, 0.0f, 0.0f)

                }
                MotionEvent.ACTION_POINTER_UP -> {
                    //还原
                    canScale=false
                    canRotate=false
                    controler.rotate(0.0f, 0.0f, 0.0f)

                }
                MotionEvent.ACTION_MOVE -> {
                    if (canRotate) {
                        if(event.pointerCount>=2){
                            controler.rotate(0.0f, 0.0f, 0.0f)
                            return@setOnTouchListener true
                        }
                        val curY = event.y
                        val curX = event.x
                        val vecX = curX - lastX
                        val vecY = curY - lastY
                        lastY = curY
                        lastX = curX
                        val distance = Math.sqrt((vecX * vecX).toDouble() + (vecY * vecY).toDouble())
                        controler.rotate(vecX, vecY, distance.toFloat())
                    } else if (canScale) {
                    //通过双指按下的距离比例来进行缩放,但是这里的比例并不实际缩放的比例,只是通过简单调整视野的范围来进行缩放
                        val index0 = event.getPointerId(0)
                        val index1 = event.getPointerId(1)
                        val curX = event.getX(index0)
                        val curY = event.getY(index0)
                        val cur1X = event.getX(index1)
                        val cur1Y = event.getY(index1)
                        val lastDis=Math.sqrt(Math.pow((lastX-last1X).toDouble(), 2.0)+Math.pow((lastY-last1Y).toDouble(), 2.0))
                        val curDis=Math.sqrt(Math.pow((curX-cur1X).toDouble(), 2.0)+Math.pow((curY-cur1Y).toDouble(), 2.0))
                        controler.scale((curDis/lastDis).toFloat())
                    }
                }
            }
            return@setOnTouchListener true;
        }

然后我们看native代码,首先是做简单的视野角度加减

void CubeTextureRender::setScale(jfloat scale) {
    LOGE("scale %f",scale);
    if (scale > 1) {
        fov -= scale;
    } else if(scale<1) {
        fov +=  (1.0f/scale);
    }
    if (fov <= 20.0) {
        fov = 20.0;
    } else if (fov > 140.0) {
        fov = 140.0;
    }
}

然后将视野传入投影即可

glm::mat4 projection = glm::mat4(1.0f);

    float ratio = (float) _backingWidth / (float) _backingHeight;

    projection = glm::perspective(glm::radians(fov), ratio, 0.1f, 100.0f);

文章到这里就结束了,除了一些必要的数学知识,其实难度不大。我们通过添加互动让立方体更加有趣。如果喜欢这篇文章,麻烦点一下关注谢谢。有什么问题也可以在评论区留言。