OpenGL ES教程——摄像机

443 阅读5分钟

image.png

上篇 坐标系统 讲述了三大矩阵:

  • model矩阵:主要指被观察物体要做的变作,变幻
  • view矩阵:观察矩阵,其实就本文要说的摄像机矩阵
  • projection矩阵:透视矩阵,这个矩阵主要作用是设视图远近,视图大小,可以放大缩小

本章主要讲view矩阵,即摄像机矩阵。

1、摄像机空间

image.png

要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右侧的向量以及一个指向它上方的向量。

  • position:即摄像机的位置,如上图所示,摄像机位置为 (0,0,2),观察点在原点

  • direction:摄像机方向,摄像机位置是在z轴正轴,观察点是原点,按理说摄像机方向应该是原点向量减去摄像机位置向量(回忆下两个向量的差的知识,向量差可以得到向量终点之间的方向向量),但图2上方向依然是指向z轴正方向,因为方向向量其实是相反的,即真实方向向量是摄像机位置向量减去观察点方向向量

  • right:代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧:先定义一个上向量(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量

glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f); 
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
  • up:已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);

2、Look At

上面的概念有点多,幸运的是,接口比较简单:

glm::mat4 view; 
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), 
    glm::vec3(0.0f, 0.0f, 0.0f), 
    glm::vec3(0.0f, 1.0f, 0.0f)
    );

lookAt函数一共有三个参数:

  • 摄像机位置
  • 观察点位置
  • 上向量(我们计算右向量使用的那个上向量)

另外,需要注意的是,lookAt产生的矩阵是view矩阵

3、旋转效果

如果我们要做一个这样的效果,该怎么做呢?

1674468845830.gif

结合上面的知识,可以让摄像机位置转圈,然后观察点位置不变即可,所以可以这样地调整view矩阵:

degree = degree + 1.0f;

float radius = 8.0f;
float temp = degree * 0.01;
float camX = sin(temp) * radius;
float camZ = cos(temp) * radius;
cameraPos = glm::vec3(camX, 0.0, camZ);
view = glm::lookAt(cameraPos,
                   cameraPos + cameraFront,
                   cameraUp);

4、自由移动

自由移动的效果如下:

1674469500142.gif

自由移动就是平面移动,它相当于一个人平视前方,然后这个人可以上下左右移动,上下移动就相当于缩放,左右移动就相当于人平视前方,因此被观察物体可能会被看不到。所以它和旋转最大的不同即是,观察点位置是变化的,摄像机位置也是变化的。为了实现这种效果,因此定义了如下矩阵:

glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); 
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); 
glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
//lookAt函数现在是
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

所以,当我们平移时,变化cameraPos向量,那观察点位置也会发生变化,就能实现平移

float cameraSpeed = 0.5f;
switch (direction) {
    case Direction::Up:
        cameraPos += cameraSpeed * cameraFront;
        break;
    case Direction::Down:
        cameraPos -= cameraSpeed * cameraFront;
        break;
    case Direction::Left:
        cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp) * cameraSpeed);
        break;
    case Direction::Right:
        cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp) * cameraSpeed);
        break;
}

5、视角移动

1674472810675.gif

视角移动的效果大致如上,能跟随手移动实现3D旋转,其实现有两种方式:

  • 欧拉角原理,通过改变view矩阵实现,相当于移动摄像机
  • 修改model矩阵,让物体自己旋转

目前欧拉角这套方式,我还未在机器上完美复现,暂时使用第2种方式实现,等后续实现了我再补上这段内容

首先当然是需要处理touch事件:

override fun onTouchEvent(event: MotionEvent): Boolean {
    when(event.actionMasked) {
        MotionEvent.ACTION_DOWN ->{
            lastX = event.x
            lastY = event.y
            canRotate = true
            canScale = false
        }
        MotionEvent.ACTION_POINTER_DOWN ->{
            canRotate = false
            canScale = true
            if (event.pointerCount > 2) {
                canScale = false
                return false
            }
            var index0 = event.getPointerId(0)
            var index1 = event.getPointerId(1)
            lastX = event.getX(index0)
            lastY = event.getY(index0)
            last2x = event.getX(index1)
            last2y = event.getY(index1)
            glNativeRender?.rorate(0f, 0f, 0f)
        }
        MotionEvent.ACTION_POINTER_UP ->{
            canRotate = false
            canScale = false
            glNativeRender?.rorate(0f, 0f, 0f)
        }
        MotionEvent.ACTION_MOVE ->{
            if (canRotate) {
                if (event.pointerCount > 2) {
                    glNativeRender?.rorate(0f, 0f, 0f)
                    return true
                }
                var curX = event.x
                var curY = event.y
                var xoffset = curX - lastX
                var yoffset = curY - lastY
                lastX = curX
                lastY = curY
                var distance = Math.sqrt((xoffset * xoffset).toDouble() + (yoffset * yoffset).toDouble())
                glNativeRender?.rorate(xoffset, yoffset, 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-last2x).toDouble(), 2.0)+Math.pow((lastY-last2y).toDouble(), 2.0))
                val curDis=Math.sqrt(Math.pow((curX-cur1X).toDouble(), 2.0)+Math.pow((curY-cur1Y).toDouble(), 2.0))
                glNativeRender?.scale((curDis/lastDis).toFloat())
            }
        }
    }
    return true
}

c++中的旋转处理:

void CubeSample::rorate(float x, float y, float distance) {
    distance = distance/80.0f;
    if (x > 0 && y > 0) {
        y = -y;
    } else if (x <= 0 && y > 0) {
        x = -x;
        distance = -distance;
    } else if (x > 0 && y < 0) {
        y = -y;
    } else if (x <= 0 && y < 0) {
        y = -y;
    }
    this->xoffset = x;
    this->yoffset = y;
    this->distance = distance;
}

注意model矩阵的处理,model矩阵必须要乘以之前的model矩阵,否则会有顿挫,因为每个动作都是叠加之前的处理,所以model矩阵要定义成成员变量:

if (distance != 0) {
    glm::vec3 cross = glm::cross(glm::vec3(0.0f, 0.0f, 1.0), glm::vec3(xoffset, yoffset, 0.0f));
    glm::mat4 tmpMat = glm::mat4(1.0f);
    model = glm::rotate(model, distance, cross);
    tmpMat *= model;
    model = tmpMat;
}

6、缩放

缩放其实就是在处理投影矩阵:

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

一般投影矩阵的第一个参数是45f,这里我们使用fov值,它可以根据缩放距离变化

c++中对绽放的处理:

void CubeSample::scale(float 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;
    }
}

在这里计算fov值,最后在透视矩阵中改变fov值,实现缩放,最后的效果为:

1674473590278.gif