摄像机在 3D 场景(国际坐标系)中的放置要确认 2 个要素:(1)方位;(2)姿势。方位用一个 vector 表达其坐标。姿势用 3 个 unit vector 表达:front, up, right,如下图。这 3 个 unit vector 依照右手规律彼此正交,因而,只需确认其间任意 2 个,进行叉积核算即得到第三个,例如: front up = right

OpenGL ES 2.0 笔记 #8:View 改换

摄像机(本地)空间以 right 为 x 方向,以 up 为 y 方向,因而,依据右手规律,front 为 -z 方向。

把摄像机方位和姿势作为程序状况,共 3 个 vector:

static struct
{
    ...
    vec3 camera_pos;
    vec3 camera_front;
    vec3 camera_up;
} render_state = {0};

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

#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 变量:

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 轴为中心的圆形轨道旋转,滚动视点依据系统时刻戳转换得到。摄像机一直朝向原点。如下:

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 轴按视点负方向(顺时针方向)旋转。如下图:

OpenGL ES 2.0 笔记 #8:View 改换

键盘操控摄像机移动

预订完成目标是:摄像机姿势不变,font 固定为 -Z 方向(向“内”),up 为 Y 方向(因而 right 固定为 X 方向);键盘按键完成摄像机左右(沿 X 轴)和远近(沿 Z 轴)方位移动。界说两个方位移动函数:

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 键盘事情时,调用这两个函数,对摄像机方位进行更新:

#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:

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 平面内。

OpenGL ES 2.0 笔记 #8:View 改换

界说 2 个功能函数,在 front/up/right 与 pitch/yaw 之间进行彼此转化:

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,即摄像机上下滚动(俯仰)。进行鼠标操作时,摄像机方位不变。

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

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()),与前面的例程坚持不变。

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

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。代码如下:

#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 度的视点改变。

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

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 鼠标事情处理部分的完成很简单:

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…

除夕高兴!!