摄像机在 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:
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 轴按视点负方向(顺时针方向)旋转。如下图:
键盘操控摄像机移动
预订完成目标是:摄像机姿势不变,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 平面内。
界说 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…
除夕高兴!!