OpenGL ES 2.0 笔记 #8:View 变换

摄像机在 3D 场景(世界坐标系)中的放置要确定 2 个要素:(1)位置;(2)姿态。位置用一个 vector 表达其坐标。姿态用 3 个 unit vector 表达:front, up, right,如下图。这 3 个 unit vector 按照右手法则相互正交,因此,只要确定其中任意 2 个,进行叉积计算即得到第三个,例如: front × up = right

摄像机(本地)空间以 right 为 x 方向,以 up 为 y 方向,因此,根据右手法则,front 为 -z 方向。

把摄像机位置和姿态作为程序状态,共 3 个 vector:

C 复制代码
static struct
{
    ...
    vec3 camera_pos;
    vec3 camera_front;
    vec3 camera_up;
} render_state = {0};

定义一个 camera_reset() 函数对摄像机状态进行初始化。在程序初始化阶段要调用该函数:

C 复制代码
#define CAMERA_MOVE_RADIUS 3.f
...

static void camera_reset()
{
    vec3 pos = {0, 0, CAMERA_MOVE_RADIUS};
    glm_vec3_copy(pos, render_state.camera_pos);

    vec3 orig = GLM_VEC3_ZERO_INIT;
    glm_vec3_sub(orig, pos, render_state.camera_front);
    glm_vec3_normalize(render_state.camera_front);

    vec3 up = {0, 1, 0}; // Y
    glm_vec3_copy(up, render_state.camera_up);
}

初始状态下,摄像机放在世界坐标系 Z 轴上((0, 0, 3)),朝向原点,以 Y 为 up 方向。注意用朝向目标点(原点)与摄像机位置进行 vector 减运算得到 front 方向上的 vector,经过 normalize 就得到 front。

进行 view 变换时,调用 cglm 的 glm_lookat() 生成变换矩阵。这个函数的参数,除了摄像机位置和 up vector 之外,还需要 front 方向上的任一位置坐标,而不是 front 本身。这个位置用摄像机位置与 front 进行 vector 加即可获得,如以下代码中的 target 变量:

C 复制代码
mat4 m1;
vec3 target;
glm_vec3_add(render_state.camera_pos, render_state.camera_front, target);
glm_lookat(render_state.camera_pos, target, render_state.camera_up, m1);

在每次渲染前,更新摄像机位置,沿着以 Y 轴为中心的圆形轨迹旋转,转动角度根据系统时间戳转换得到。摄像机始终朝向原点。如下:

C 复制代码
float a = (float)(SDL_GetTicks() / 1000.);
vec3 v1 = {
    CAMERA_MOVE_RADIUS * sin(a),
    0,
    CAMERA_MOVE_RADIUS * cos(a),
};
glm_vec3_copy(v1, render_state.camera_pos);

glm_vec3_negate(v1) ;
glm_vec3_normalize( v1 ) ;
glm_vec3_copy( v1, render_state.camera_front) ;

这里计算 front 时换了一种算法,利用摄像机朝向原点这一特殊条件,将摄像机位置取反并 normalize 就得到 front。

完整代码见 gitlab.com/sihokk/lear...

程序中摄像机绕 Y 轴按角度正方向(逆时针)旋转,程序运行时,呈现的效果则是 3D 模型绕 Y 轴按角度负方向(顺时针方向)旋转。如下图:

键盘控制摄像机移动

预定实现目标是:摄像机姿态不变,font 固定为 -Z 方向(向"内"),up 为 Y 方向(因此 right 固定为 X 方向);键盘按键实现摄像机左右(沿 X 轴)和远近(沿 Z 轴)位置移动。定义两个位置移动函数:

C 复制代码
static void camera_move_right(float dist)
{
    render_state.camera_pos[0] += dist;
}

static void camera_move_forward(float dist)
{
    render_state.camera_pos[2] -= dist;
}

接收到 SDL 键盘事件时,调用这两个函数,对摄像机位置进行更新:

C 复制代码
#define CAMERA_MOVE_SPEED 0.01f // distance in 1 ms
...

// Key press: camera movement
if (SDL_KEYDOWN == e.type)
{
    const SDL_Keycode k = e.key.keysym.sym;

    if (SDLK_w == k)
    {
        const float dist = CAMERA_MOVE_SPEED * render_state.frame_interval;
        camera_move_forward(dist);
    }
    else if (SDLK_s == k)
    {
        const float dist = CAMERA_MOVE_SPEED * render_state.frame_interval;
        camera_move_forward(-dist);
    }
    else if (SDLK_a == k)
    {
        const float dist = CAMERA_MOVE_SPEED * render_state.frame_interval;
        camera_move_right(-dist);
    }
    else if (SDLK_d == k)
    {
        const float dist = CAMERA_MOVE_SPEED * render_state.frame_interval;
        camera_move_right(dist);
    }

    ...
}

if (SDL_KEYUP == e.type)
{
    const SDL_Keycode k = e.key.keysym.sym;

    if (SDLK_PERIOD == k || SDLK_KP_PERIOD == k)
    {
        camera_reset();
    }

    ...
}

按下 A, D 键时,摄像机将分别向左(-X)右(X)移动;按下 W, S,则分别向内(-Z)外(Z)移动。移动保持匀速,根据当前帧与上一帧的时间间隔计算出来。另外,按下 .(句号)键将摄像机重置到初始位置。

帧间隔由下面的函数实现,在主循环中每次渲染前调用。时间单位为 ms:

C 复制代码
static struct
{
    ...
    uint32_t frame_interval;
} render_state = {0};

static void calc_frame_interval()
{
    static struct
    {
        bool valid;
        uint32_t timestamp;
    } last_frame = {false};

    const uint32_t now = SDL_GetTicks();

    if (last_frame.valid)
    {
        render_state.frame_interval = now - last_frame.timestamp;
        last_frame.timestamp = now;
    }
    else
    {
        last_frame.valid = true;
        last_frame.timestamp = now;
        render_state.frame_interval = 0;
    }
}

完整代码见 gitlab.com/sihokk/lear...

鼠標控制攝像機轉向

如下图,摄像机姿态可由 2 个角度确定:(1)front 与 X-Z 平面的夹角为仰角 pitch;(2)right 与 X 轴之间的夹角为偏航角 yaw。利用三角函数,front/up/right 与 pitch/yaw 之间可以相互转化:

  • sin(pitch) = front.y
  • sin(yaw) = - right.z

注意到,right 始终位于 X-Z 平面内。

定义 2 个功能函数,在 front/up/right 与 pitch/yaw 之间进行相互转化:

C 复制代码
static void get_eular_angles(const float *front, const float *up, float *pitch, float *yaw)
{
    float pitch1 = asinf(front[1]);
    if (isnan(pitch1))
    {
        pitch1 = front[1] > 0 ? GLM_PI_2f : (-GLM_PI_2f);
    }

    *pitch = pitch1;

    vec3 right;
    glm_vec3_cross(front, up, right);
    glm_vec3_normalize(right);

    float yaw1 = asinf(-right[2]);

    if (isnan(yaw1))
    {
        yaw1 = right[2] > 0 ? (-GLM_PI_2f) : GLM_PI_2f;
    }

    if (right[0] < 0)
    {
        yaw1 = GLM_PIf - yaw1;
    }
    if (yaw1 < 0)
    {
        yaw1 += (2 * GLM_PIf);
    }

    *yaw = yaw1;
}

static void set_eular_angles(float *front, float *up, float pitch, float yaw)
{
    vec3 right = {
        cosf(yaw),
        0,
        -sinf(yaw),
    };

    float len_p1 = cosf(pitch);
    vec3 front1 = {
        len_p1 * cosf(yaw + GLM_PI_2f),
        sinf(pitch),
        -sinf(yaw + GLM_PI_2f),
    };

    glm_vec3_normalize(front1);
    glm_vec3_copy(front1, front);

    vec3 up1;
    glm_vec3_cross(right, front1, up1);
    glm_vec3_normalize(up1);
    glm_vec3_copy(up1, up);
}

通过鼠标事件更新摄像机的 pitch/yaw 姿态角。当鼠标在窗口上进行拖动(按下鼠标左键后移动)操作时,水平方向移动距离确定摄像机的 yaw,即摄像机左右转动;垂直方向的移动距离确定 pitch,即摄像机上下转动(俯仰)。进行鼠标操作时,摄像机位置不变。

在程序状态中增加了鼠标相关数据:

C 复制代码
static struct
{
    ...
    struct
    {
        bool active;
        int32_t begin_x;
        int32_t begin_y;
        float32_t begin_pitch;
        float32_t begin_yaw;
        ...
    } mouse_drag;
} render_state = {0};

实现的思路是这样的。在开始拖放时,记下鼠标的位置,以及根据此时摄像机的姿态(front/up)计算出的姿态角 pitch/yaw。在拖放过程中,由鼠标当前位置与初始位置计算出移动的距离,按照一定规则将此距离转换为 pitch/yaw 的变化值,对 pitch/yaw 进行更新,之后将其转换回 front/up 状态,并进行渲染。

姿态角 pitch/yaw 只是在拖动操作的过程中便于直接进行角度计算。在 OpenGL view 变换时,仍以 font/up 参与计算(glm_lookat()),与前面的例程保持不变。

当鼠标左键按下时,开始拖放操作,对应的处理函数如下:

C 复制代码
static void camera_drag_begin(int32_t x, int32_t y)

{
     render_state.mouse_drag.active = true;

    render_state.mouse_drag.begin_x = x;
    render_state.mouse_drag.begin_y = y;
    ...

    get_eular_angles(render_state.camera_front, render_state.camera_up,
        &render_state.mouse_drag.begin_pitch,
        &render_state.mouse_drag.begin_yaw);
}

拖放过程中,根据鼠标移动的距离,按照一定关系转换为角度的变化量,更新 pitch 和 yaw 后,计算出摄像机 front/up。代码如下:

C 复制代码
#define CAMERA_DRAG_FOV 90 // in degrees

static void camera_drag(int32_t x, int32_t y)
{
    int32_t dx, dy;
    ...

    dx = x - render_state.mouse_drag.begin_x;
    dy = y - render_state.mouse_drag.begin_y;

    float speed = CAMERA_DRAG_FOV / glm_min(render_state.screen_width, render_state.screen_height);
    speed = glm_rad(speed);

    float pitch = render_state.mouse_drag.begin_pitch - speed * dy;
    float yaw = render_state.mouse_drag.begin_yaw - speed * dx;

    set_eular_angles(render_state.camera_front, render_state.camera_up, pitch, yaw);
}

这里以窗口绘制区域较短一条边长为基准,鼠标移动该边长的距离则对应 90 度的角度变化。

相较于上一个示例程序,由于摄像机朝向发生了变化,将键盘移动摄像机位置的代码进行了修改,使用了常规的矢量运算,如下:

C 复制代码
static void camera_move_right(float dist)
{
    vec3 right;
    glm_vec3_cross(render_state.camera_front, render_state.camera_up, right);
    glm_normalize(right);
    glm_vec3_scale(right, dist, right);
    glm_vec3_add(render_state.camera_pos, right, render_state.camera_pos);
}

static void camera_move_forward(float dist)
{
    vec3 front;
    glm_vec3_copy(render_state.camera_front, front);
    glm_vec3_scale(front, dist, front);
    glm_vec3_add(render_state.camera_pos, front, render_state.camera_pos);
}

SDL2 鼠标事件处理部分的实现很简单:

C 复制代码
while(1)
{
    ...
    
    // Left mouse button down
    if (SDL_MOUSEBUTTONDOWN == e.type && SDL_BUTTON_LEFT == e.button.button)
    {
        camera_drag_begin(e.button.x, e.button.y);
        continue;
    }

    // Left mouse button release
    if (SDL_MOUSEBUTTONUP == e.type && SDL_BUTTON_LEFT == e.button.button)
    {
        camera_drag_end();
        continue;
    }

    // Mouse drag
    if (SDL_MOUSEMOTION == e.type)
    {
        if (!render_state.mouse_drag.active)
        {
            continue;
        }

        camera_drag(e.motion.x, e.motion.y);
        continue;
    }
    
    ...
}

完整代码请参考 gitlab.com/sihokk/lear...

除夕快乐!!

相关推荐
扶尔魔ocy12 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行并进行态势标绘(四)
qt·opengl·osg
:mnong13 天前
Web 三维可视化技术对比:ParaView Web vs 本项目 wasm + Three.js
前端·webgl·opengl·opencl·科学可视化
扶尔魔ocy14 天前
【qt openGL入门笔记】OSG实现显示飞机模型飞行的点迹(二)
qt·opengl·osg
扶尔魔ocy15 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行(三)
opengl·osg
扶尔魔ocy16 天前
【qt openGL入门笔记】OSG实现飞机模型在网格图上飞行
qt·opengl·osg
扶尔魔ocy19 天前
【qt openGL入门笔记】FPS相机
qt·opengl
扶尔魔ocy20 天前
qt openGL入门笔记】FPS相机和轨道相机
qt·opengl
扶尔魔ocy21 天前
【qt openGL入门笔记】基础光照
qt·opengl
扶尔魔ocy22 天前
【qt openGL入门笔记】坐标系统和摄像机
qt·opengl
茉莉玫瑰花茶24 天前
OpenGL [ 纹理 ]
开发语言·c++·opengl