一看就懂的OpenGL ES教程-3D渲染实战

我正在参加「掘金·启航计划」

通过阅读本文,你将获得以下收获:
1.如何渲染3D纹理
2.如何渲染一个多纹理的立方体
3.如何渲染多个立方体并且提供交互操作

上篇回顾

上2篇博文一看就懂的OpenGL ES教程——走进3D的世界之坐标系统(上篇)一看就懂的OpenGL ES教程——走进3D的世界之坐标系统(下篇) 已经比较详细地将3D物体如何渲染到2D平面的过程描述一遍,即通过坐标系的转换(模型变换-》视图变换-》投影变换-》视口变换),将原始顶点坐标位置,变换到一个模拟出眼睛看物体的效果

温馨提示:如果还没有看过前面2篇博文,那么强烈建议先看下前面2篇博文,不然本文可能完全看不懂~

前两篇博文是纯理论篇,大家看的过程中难免感觉枯燥,估计能坚持看到最后的小伙伴都不多。如果说前两篇博文是良药苦口,那么今天的内容就如同炎炎夏日来一杯百事可乐一样爽快~~

渲染3D纹理

相信各位还记得讲渲染纹理的这篇博文吧一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(实践篇),当时是按照2D的方式渲染的,那今天就来看看如果渲染出一个3D效果的纹理。

关于如何渲染2D纹理这里就不再赘述了,不清楚的童鞋可以再去看看上面这篇博文。

直接从处理3D部分讲起,3D的核心就是处理那几个变换矩阵。有童鞋可能会问了,前两篇的变换矩阵推导那么复杂,那么会不会每次需要使用到变换矩阵的时候都要算一遍?

答案显然不是。在软件开发行业发展如此发达的年代,这种复杂的计算过程往往已经有优秀的库帮程序员分担的,而这次帮我们分担工作量的依然是glm

在之前写的博文一看就懂的OpenGL ES教程——仿抖音滤镜的奇技淫巧之变换滤镜(实践篇)中就已经见识过glm的好了,今天大家可以进一步感受下glm的“温暖”。

        #version 300 es



        layout (location = 0) in vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段\n"//如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0



        layout (location = 1) in vec2 aTextCoord;//输入的纹理坐标,会在程序指定将数据输入到该字段

        //输出的纹理坐标;

        out vec2 TexCoord;

        //模型矩阵

        uniform mat4 model;


        //观察矩阵

        uniform mat4 view;

        //投影变换矩阵

        uniform mat4 projection;



        void main() {

           //这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来)

           TexCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);

           //原始顶点坐标按顺序左乘模型矩阵、观察矩阵、投影矩阵

           gl_Position = projection * view * model * aPosition;

        };

首先看看顶点着色器,从3D到2D的变换处理都在这里。我们需要的是模型矩阵、观察矩阵、投影矩阵,在顶点着色器中定义为uniform变量。然后让原始顶点坐标按顺序左乘模型矩阵、观察矩阵、投影矩阵,得到最终真正在2D平面中显示的顶点坐标(当然还有视口变换,不过这个OpenGL有直接提供api)。

片段着色器依然是最简单的纹理映射代码:

          #version 300 es

          precision mediump float;

          in vec2 TexCoord;

          out vec4 FragColor;

          //传入的纹理

          uniform sampler2D ourTexture;




          void main() { 

             FragColor = texture(ourTexture, TexCoord);

           };

在C++代码中,需要做的是通过glm得到对应的模型矩阵、观察矩阵、投影矩阵,并传给顶点着色器。

//模型矩阵,将局部坐标转换为世界坐标
glm::mat4 model = glm::mat4(1.0f);
//视图矩阵,确定物体和摄像机的相对位置
glm::mat4 view = glm::mat4(1.0f);
//透视投影矩阵,实现近大远小的效果
glm::mat4 projection = glm::mat4(1.0f);
//沿着x轴旋转
model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(1.0f, 0.0f, 0.0f));
// 注意,我们将矩阵向我们要进行移动场景的反方向移动。(右手坐标系,所以z正方从屏幕指向外部)
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
//这里假设视场角为45度,视口的宽高直接取屏幕宽高,近平面距离取0.1,远平面距离取100
projection = glm::perspective(glm::radians(45.0f), (float) screenWidth / (float) screenHeight,
                              0.1f,
                              100.0f);
//将矩阵传递给shader
GLint modelLoc = glGetUniformLocation(program, "model");
GLint viewLoc = glGetUniformLocation(program, "view");
GLint projectionLoc = glGetUniformLocation(program, "projection");

glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));

模型变换简单点,就让纹理绕x轴逆时针旋转-45度,glm::radians(-45.0f)为将其转为弧度。

视图变换,也简单点,假设相机就放在纹理的前方3个单位距离位置,也就是纹理在摄像机的负z方向距离摄像机3个单位,所以平移向量为(0.0f, 0.0f, -3.0f)。

接下来的透视投影矩阵我们知道是最为复杂的,所幸glm已经有直接提供透视投影矩阵的方法了:

template <typename T>
GLM_FUNC_DECL tmat4x4<T, defaultp> perspective(
   T fovy,
   T aspect,
   T near,
   T far);

这是一个泛型函数,返回透视投影需要的变换矩阵。返回值的类型tmat4x4<T, defaultp>是一个类型定义,它是GLM库中的一个模板类,用于表示一个4×4的矩阵。该类型定义包含两个模板参数:

– T:矩阵元素的类型,可以是浮点数、整数等。
– defaultp:矩阵的存储精度,默认为单精度浮点数。

该函数接受四个参数:

– fovy:视场角,以弧度为单位。
– aspect:视口的宽高比。
– zNear:近平面的距离。
– zFar:远平面的距离。

image.png

这里假设视场角为45度,视口的宽高直接取屏幕宽高,近平面距离取0.1,远平面距离取100。

projection = glm::perspective(glm::radians(45.0f), (float) screenWidth / (float) screenHeight,
                              0.1f,
                              100.0f);

运行代码可得:

Screenshot_20230624_183937.jpg

可以看到,原来平铺的纹理顿时有了立体感,向后倾斜,并且有近大远小的感觉。

渲染立方体

接下来,我们来画一个很有意思的正方体,染上酷炫的渐变色。

顶点着色器和上面那绘制3D纹理的几乎一样,只是多了一个颜色的输入变量。

        #version 300 es



        layout (location = 0) in vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段//如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0
        layout (location = 1) in vec4 aColor;//输入的颜色,会在程序指定将数据输入到该字段



        out vec4 vTextColor;//输出的颜色


        out vec2 TexCoord;//输出的纹理坐标;
        uniform mat4 model;


        uniform mat4 view;
        uniform mat4 projection;



        void main() {
            //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
            gl_Position = projection * view * model * aPosition;
            vTextColor = aColor;
        };

片段着色器很简单,只是将输入颜色赋值给FragColor

        #version 300 es



        precision mediump float;
        in vec4 vTextColor;//输入的颜色
        out vec4 FragColor;
        //传入的纹理
        void main() {
             FragColor = vTextColor;
        };

因为是立方体,共有8个点,所以顶点数组就比较复杂了:

float vertices[] = {

        // 顶点坐标                 颜色
        -0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.0f,//背后左下角点 0
        0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.0f,//背后右下角点 1


        0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 1.0f,//背后右上角点 2
        -0.5f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f,//背后左上角点 3

        -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.0f,//前面左下角点 4
        0.5f, -0.5f, 0.5f, 0.0f, 1.0f, 1.0f,//前面右下角点 5



        0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f,//前面右上角点 6
        -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.0f,//前面左上角点 7

};

我们用EBO来渲染立方体的6个面,那么立方体的6个面可以如下表示:

unsigned int indices[] = {
        //背面
        0, 3, 1, // first triangle
        3, 2, 1, // second triangle
        //上面
        2, 3, 7, // first triangle
        7, 6, 2,  // second triangle
        //左面
        3, 0, 4, // first triangle
        4, 7, 3, // second triangle
        //右面
        5, 1, 2, // first triangle
        2, 6, 5, // second triangle
        //下面
        4, 0, 1, // first triangle
        1, 5, 4,// second triangle
        //前面
        4, 5, 6, // first triangle
        6, 7, 4, // second triangle
};

其实渲染代码和上面那渲染纹理的基本一样了,不过为了增加乐趣,我让它旋转起来:

//f表示不断变化的旋转角度,每一帧就变化1度
float f = 0.0f;
while (f >= 0) {
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    //清除深度缓冲和颜色缓冲,开始渲染全新的一帧
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    //模型矩阵,将局部坐标转换为世界坐标
    glm::mat4 model = glm::mat4(1.0f);
    //视图矩阵,确定物体和摄像机的相对位置
    glm::mat4 view = glm::mat4(1.0f);
    //透视投影矩阵,实现近大远小的效果
    glm::mat4 projection = glm::mat4(1.0f);
    //沿着向量(0.5f, 1.0f, 0.0f)旋转
    model = glm::rotate(model, glm::radians(f), glm::vec3(0.5f, 1.0f, 0.0f));
    // 注意,我们将矩阵向我们要进行移动场景的反方向移动。(右手坐标系,所以z正方形从屏幕指向外部)
    view = glm::translate(view, glm::vec3(0.0f, 0.0f, -5.0f));
   
    projection = glm::perspective(glm::radians(45.0f),
                                  (float) screen_width / (float) screen_height, 0.1f,
                                  100.0f);


    GLint modelLoc = glGetUniformLocation(program, "model");
    GLint viewLoc = glGetUniformLocation(program, "view");
    GLint projectionLoc = glGetUniformLocation(program, "projection");
    
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
   
    glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

    //窗口显示,交换双缓冲区
    eglSwapBuffers(display, winSurface);
    //每一帧就变化1度
    f++;
    //间隔0.4秒渲染一帧
    sleep(static_cast<unsigned int>(0.4));
}

这里变量f表示不断变化的旋转角度,每一帧就变化1度,间隔0.4秒渲染一帧,这样就有动画的效果,运行效果如下:

cube1.gif

perfect~酷毙了!

不过,怎么看起来很奇怪的样子?

本来应该被遮挡住的面,反而能够看到,显然不符合实际情况。原因是少开了深度测试,关于深度测试,曾经在博文一看就懂的OpenGL ES教程——图形渲染管线的那些事末尾讲过,不知大家是否有印象,简单来说,就是记录每个片段的深度,对于不透明物体,在渲染的时候只渲染深度最小的,这样就符合我们看东西前面的物体会挡住后面物体的情景,又可以节省不必要的性能开销。

具体来说,深度缓冲是在片段着色器运行之后在屏幕空间中运行的。深度缓冲就像颜色缓冲(Color Buffer)(储存所有的片段颜色:视觉输出)一样,在每个片段中储存了信息,并且(通常)和颜色缓冲有着一样的宽度和高度。深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。

当深度测试(Depth Testing)被启用的时候,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比。OpenGL会执行一个深度测试,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃

开启深度测试只需要一行代码:

glEnable(GL_DEPTH_TEST);

还有就是要在渲染每帧之前,就像清空颜色缓冲一样在使用glClear方法加上GL_DEPTH_BUFFER_BIT表示清空深度缓冲:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

开启深度测试之后运行:

cube.gif

这下就正常了~

渲染多纹理的立方体

再来玩点更有趣的东西。能不能立方体每个面都渲染上不同纹理呢?答案是肯定的。

顶点着色器直接使用上面渲染一个3D纹理的代码即可,毕竟只是将1一个纹理改为渲染6个:

        #version 300 es



        layout (location = 0) in vec4 aPosition;//输入的顶点坐标,会在程序指定将数据输入到该字段\n"//如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0



        layout (location = 1) in vec2 aTextCoord;//输入的纹理坐标,会在程序指定将数据输入到该字段

        //输出的纹理坐标;

        out vec2 TexCoord;

        //模型矩阵

        uniform mat4 model;


        //观察矩阵

        uniform mat4 view;

        //投影变换矩阵

        uniform mat4 projection;



        void main() {

           //这里其实是将上下翻转过来(因为安卓图片会自动上下翻转,所以转回来)

           TexCoord = vec2(aTextCoord.x, 1.0 - aTextCoord.y);

           //原始顶点坐标按顺序左乘模型矩阵、观察矩阵、投影矩阵

           gl_Position = projection * view * model * aPosition;

        };

片段着色器同样可以使用渲染一个3D纹理的代码:

          #version 300 es

          precision mediump float;

          in vec2 TexCoord;

          out vec4 FragColor;

          //传入的纹理

          uniform sampler2D ourTexture;




          void main() { 

             FragColor = texture(ourTexture, TexCoord);

           };

C++层中,先定义6个面:

float vertices[] = {

        // 顶点坐标           纹理坐标
        //背面
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,  //2
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,  //1
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f, //0
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,  //0
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,  //3
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,  //2


        //前面
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,//4
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,//5
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,//6
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,//6
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,//7
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,//4

        //左面
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,//7
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,//3
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,//0
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,//0
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,//4
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,//7

        //右面
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,//1
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,//2
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,//6
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,//6
        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,//5
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,//1

        //底面
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,//0
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,//1
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,//5
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,//5
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,//4
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,//0

        //上面
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,//6
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,//2
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,//3
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,//3
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,//7
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,//6

};

可以看到,由于每个面都要渲染一个纹理,所以每个面的点都对应一个纹理坐标

VBO, VAO的配置代码如下:

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);



    glBindVertexArray(VAO);


    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 读取顶点属性配置
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *) 0);
    glEnableVertexAttribArray(1);
    // 读取纹理坐标配置
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
                          (void *) (3 * sizeof(float)));
    glEnableVertexAttribArray(1);

然后先把Java层传入的6个Bitmap的图片信息对象保存起来:

//持有图片信息类BitmapInfo的集合,这里是保存6个面的纹理对应的图片信息
std::vector<BitmapInfo> bitmapVector;
//bitmaps为从Java层传入的6个Bitmap对象
jsize ref_size = env->GetArrayLength(bitmaps);
for (int i = 0; i < ref_size; ++i) {
    jobject bitmap = env->GetObjectArrayElement(bitmaps, i);
    AndroidBitmapInfo bmpInfo;
    BitmapInfo bitmapInfo(env,bitmap,bmpInfo);
    bitmapVector.emplace_back(bitmapInfo);


}

目前6个面的纹理渲染还是一样的处理,所以只要复用到一个纹理对象即可。以下是纹理对象的配置:

unsigned int texture1;
//-------------------- texture1的配置start ------------------------------
glGenTextures(1, &texture1);
glBindTexture(GL_TEXTURE_2D, texture1);
// set the texture wrapping parameters(配置纹理环绕)
//横坐标环绕配置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,
                GL_REPEAT);    // set texture wrapping to GL_REPEAT (default wrapping method)
//纵坐标环绕配置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// set texture filtering parameters(配置纹理过滤)
//纹理分辨率大于图元分辨率,即纹理需要被缩小的过滤配置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
//纹理分辨率小于图元分辨率,即纹理需要被放大的过滤配置
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// load image, create texture and generate mipmaps
//这里指定纹理尺寸为1280*720,所以使用到的图片尺寸也必须符合这个尺寸
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1280, 720, 0, GL_RGBA,
             GL_UNSIGNED_BYTE, NULL);
//-------------------- texture1的配置end ------------------------------


//对着色器中的纹理单元变量进行赋值
glUniform1i(glGetUniformLocation(program, "ourTexture"), 0);

变换矩阵的处理和上面渲染立方体例子是一模一样的,不同的是这里需要渲染6个面的纹理:

 for (int i = 0; i < 36; i = i + 6) {
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, texture1);



            int index = i/6;
            //取出每个纹理的图片信息
            BitmapInfo bmpInfo = bitmapVector[index];
            void *bmpPixels;
            AndroidBitmap_lockPixels(env, bmpInfo.bitmap, &bmpPixels);
            //替换纹理,比重新使用glTexImage2D性能高
            glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, bmpInfo.bmpInfo.width, bmpInfo.bmpInfo.height, GL_RGBA,
                            GL_UNSIGNED_BYTE,
                            bmpPixels);
            AndroidBitmap_unlockPixels(env, bmpInfo.bitmap);
            //每6个点渲染一个面
            glDrawArrays(GL_TRIANGLES, i, 6);
        }

就是取出6个面的图片信息,然后通过glTexSubImage2D方法进行纹理的渲染,如果对这个方法不熟悉,可以看下之前写的博文,里面有详细介绍:
一看就懂的OpenGL ES教程——渲染宫崎骏动漫重拾童年

运行下:

cube.gif

简直帅到没朋友了,我愿意称它为旋转的童年~

渲染多个立方体

现在我们要画10个立方体,分别在不同的位置,要怎么做呢。

想一下我们之前讲过的坐标系统,和单个物体位置坐标相关的就是局部坐标系到世界坐标系的转换了,原本自己的自拍是以自己为中心坐标点的,现在要拍合照,那么每个人都要变换到另一个坐标系。

所以10个立方体只需要分别传不同的模型矩阵即可:

//每个立方体的平移向量
glm::vec3 cubePositions[] = {
        glm::vec3( 2.0f,  5.0f, -15.0f),
        glm::vec3(-1.5f, -2.2f, -2.5f),
        glm::vec3(-3.8f, -2.0f, -12.3f),
        glm::vec3 (2.4f, -0.4f, -3.5f),
        glm::vec3(-1.7f,  3.0f, -7.5f),
        glm::vec3( 1.3f, -2.0f, -2.5f),
        glm::vec3( 1.5f,  2.0f, -2.5f),
        glm::vec3( 1.5f,  0.2f, -1.5f),
        glm::vec3(-1.3f,  1.0f, -1.5f),
        glm::vec3( 0.0f,  0.0f,  0.0f),
        
        ……
        
        
 for (unsigned int i = 0; i < 10; i++) {
            //先每个立方体做平移变换
            glm::mat4 model = glm::mat4(1.0f);
            model = glm::translate(model, cubePositions[i]);
            //每个立方体再做不通的旋转变换
            switch (i % 3) {
                case 0:
                    model = glm::rotate(model, glm::radians(f * 2), glm::vec3(1.0f, 0.3f, 0.5f));
                    break;
                case 1:
                    model = glm::rotate(model, glm::radians(f), glm::vec3(1.0f, 1.0f, 0.5f));
                    break;
                case 2:
                    model = glm::rotate(model, glm::radians(f * 1.5f), glm::vec3(0.5f, 0.0f, 0.5f));
                    break;
            }
            //传给着色器
            glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
            //绘制立方体
            glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
    }
};

每个立方体的模型矩阵应用不同的平移和旋转变换,这样10个立方体看起来就完全不一样的位置和”姿态”了。

运行一下:

cube.gif

更加酷毙~

交互操作

如果要实现旋转摄像机观看角度观察这些立方体的效果,要怎么处理呢?

经过上面的例子,要旋转立方体处理的是模型矩阵,那么改变摄像机观看角度那就是改变观察矩阵了,这也能体会到将变换拆分为多个步骤的好处了吧。

在之前的博文一看就懂的OpenGL ES教程——走进3D的世界之坐标系统(上篇)曾经讲过从世界坐标变换到以摄像机为原点的观察空间坐标系,需要知道摄像机的位置以及以摄像机为原点建立起来的坐标系的3个坐标轴在世界坐标中的对应向量。在上面的例子里,只是简单地把原点从世界坐标系原点进行平移就得到摄像机坐标系原点位置,即假设摄像机的3个轴是分别和世界坐标系的3个坐标轴平行的,但实际上不是这么简单的,摄像机可能会有各种旋转,所以3个轴不一定和世界坐标系的3个坐标轴平行。博文一看就懂的OpenGL ES教程——走进3D的世界之坐标系统(上篇)中得到的观察矩阵为:

image.png

其中R\color{red}R是摄像机的右向量即x轴,U\color{green}U是摄像机的上向量即y轴,D\color{blue}D是摄像机的方向向量z轴,P\color{purple}P是摄像机位置向量。

不过用代码来表示这个观察矩阵还是太麻烦了,好在贴心的glm又有提供直接使用的方法:

template <typename T, precision P>
GLM_FUNC_DECL tmat4x4<T, P> lookAt(
   tvec3<T, P> const & eye,
   tvec3<T, P> const & center,
   tvec3<T, P> const & up);

eye:表示摄像机在世界坐标系中的位置。
center:表示摄像机看向的点。
up:表示摄像机的上方向向量。

什么叫做摄像机看向的点呢,比如下图,摄像机看向的点就是世界坐标系的原点:

image.png

要实现出一种摄像机在改变观看角度的效果,其实就是改变摄像机看过去的目标点,也就是摄像机的方向向量。因为要根据触摸而改变,所以将摄像机相关的位置和向量都定义为变量。

假设摄像机的位置、方向向量、上方向向量分别为:

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 10.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -10.0f);
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);

则摄像机看过去的目标点为:cameraPos + cameraFront。

则构造出来的观察矩阵为:

glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

后续的渲染逻辑也是和前面例子一样,现在关键就是如何通过触摸屏幕去改变摄像机观看的角度。

我们知道二维空间中的旋转只需要一个角度即可,那三维空间的旋转呢?

欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:

image.png

俯仰角(Pitch)表示垂直于x轴的旋转,偏航角(Yaw)表示垂直于y轴的旋转,滚转角(Roll)表示垂直于z轴的旋转。

现在我们要求的,就是这三个角度变化对于方向向量的影响

一般对于摄像机系统来说,我们只关心俯仰角和偏航角,所以我们不会讨论滚转角(想象下一个第一人称游戏的视角的滚转角总在变会有多难受)。

即求给定一个俯仰角和偏航角,如何把它们转换为一个代表新的方向向量的3D向量。

我们先复习下基础的三角函数知识:

image.png

如果我们把斜边边长定义为1,我们就能知道邻边的长度是cos x/h=cos x/1=cos x
,它的对边是sin y/h=sin y/1=sin y。

对于俯仰角Pitch来说,我们可以从xz轴所在平面看向Y轴,假设方向向量为单位向量,则如图:

image.png

假设Pitch角为θ\theta,则由三角函数基础可得对于一个给定俯仰角的y值等于sin θ\sin\ \theta

所以俯仰角和方向向量的y分量的关系为:

direction.y = sin(glm::radians(pitch)); // 注意我们先把角度转为弧度

同理我们可以只看xz平面,看偏航角(Yaw)和方向向量的x、z分量的关系,可得到:

image.png

就像俯仰角的三角形一样,我们可以看到x分量取决于cos(yaw)的值,z值同样取决于(yaw)的值,而此时斜边是单位长度的方向向量在xz平面的投影,即长度为cos(pitch),所以可得方向向量的x、z分量为:

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

因为是要通过触摸屏幕改变摄像机的观看方向,所以就需要老生常谈的onTouchEvent方法了,我们新建一个Native方法去给Java的onTouchEvent方法调用:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_openglstudydemo_YuvPlayer_handleTouchEvent(JNIEnv *env, jobject thiz, jint action,
                                                            jfloat xpos, jfloat ypos) {


    TouchCtlCamera_LOGD("handleTouchEvent action:%d,xpos:%f,ypos:%f:",action, xpos, ypos);



    if (touchCtlCamera == nullptr){
        return;
    }



    switch (action) {
        case TouchActionMode::ACTION_UP:
            touchCtlCamera->lastX = 0.0f;
            touchCtlCamera->lastY = 0.0f;
            break;
        case TouchActionMode::ACTION_CANCEL:
            touchCtlCamera->lastX = 0.0f;
            touchCtlCamera->lastY = 0.0f;
            break;
        case TouchActionMode::ACTION_DOWN:
            //每次手指按下,就用lastX和lastY保存起来
            touchCtlCamera->lastX = xpos;
            touchCtlCamera->lastY = ypos;
            break;
        case TouchActionMode::ACTION_MOVE:
            //算出当前和上一次触摸点移动的距离,即滑动距离
            float xoffset = xpos - touchCtlCamera->lastX;
            float yoffset = touchCtlCamera->lastY - ypos;
            touchCtlCamera->lastX = xpos;
            touchCtlCamera->lastY = ypos;
            //摄像机的移动敏感度,即手指滑动对摄像机移动角度的影响
            float sensitivity = 0.02;
            xoffset *= sensitivity;
            yoffset *= sensitivity;

            //根据触摸的偏移量计算出角度的变化
            touchCtlCamera->yaw += xoffset;
            touchCtlCamera->pitch += yoffset;

            if (touchCtlCamera->pitch > 89.0f) {
                touchCtlCamera->pitch = 89.0f;
            }

            if (touchCtlCamera->pitch < -89.0f) {
                touchCtlCamera->pitch = -89.0f;
            }

            //计算出角度变化导致的方向向量的变化
            glm::vec3 front;
            front.x = cos(glm::radians(touchCtlCamera->yaw)) * cos(glm::radians(touchCtlCamera->pitch));
            front.y = sin(glm::radians(touchCtlCamera->pitch));
            front.z = sin(glm::radians(touchCtlCamera->yaw)) * cos(glm::radians(touchCtlCamera->pitch));
            touchCtlCamera->cameraFront = glm::normalize(front);
            break;

    }

cube.gif

wonderful,有点打游戏的感觉了!

总结

今天破纪录写了超过2万字了,不过内容确实挺过瘾的,从简单的3D效果的渲染一张图片,到渲染旋转立方体,再到给立方体每一面渲染不同的纹理,最后到渲染多个立方体并且提供触摸交互操作,也算是体验到3D渲染的快感了,后面章节,就可以开始接触光照相关的知识了。

项目代码

opengl-es-study-demo 不断更新中,欢迎各位来star~

参考:

GAMES101-现代计算机图形学入门-闫令琪
变换
Fundamentals of Computer Graphics, Fourth Edition
计算机图形学系列笔记
坐标系统

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

系列文章目录

体系化学习系列博文,请看音视频系统学习总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

一看就懂的OpenGL es教程

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYs1c1kE' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片