最近做的一个题目要求用openGL实现一个漫游功能,虽然不知道这个漫游是不是指第一人称(其实我觉得第三人称俯视的那种也算),不过都差不多
主要使用openGL的gluLookAt函数,通过计算球面坐标来实现
目录
gluLookAt()
实现过程
demo
最终效果
void gluLookAt( GLdouble eyex, GLdouble eyey, GLdouble eyez, GLdouble centerx, GLdouble centery, GLdouble centerz, GLdouble upx, GLdouble upy, GLdouble upz);
可以看到gluLookAt函数中有三组三维坐标
第一组eye xyz是摄像机的位置,第二组center xyz是摄像机看向的位置,第三组up xyz是向上向量
注意第一组和第二组是坐标,而第三组是向量
可以这么理解
第一组:脑袋/眼睛的位置
第二组:眼睛看向的位置
第三组:头顶的方向(躺着和坐着看一个物体,看到的景象是不同的)
一开始有人跟我这么解释“up为头顶方向”的时候,我并不理解,觉得如果抬头看一个物体的话,那么头顶的位置不也得向后倾斜?emmm,其实头顶方向这个说法并不严谨,应该说你正视前方时头顶的方向,或者你理解成看东西的时候眼珠子动脑袋不动
就比如上面这两个小人,两人眼睛的位置是相同的(你可以认为这个人是独眼怪),眼睛看向的位置是相同的,但头顶的方向是不同的,看到的图像也是不同的
实现的思路是根据鼠标相对于屏幕中心点的距离来控制转头的幅度
glutPassiveMotionFunc(yourFunc0);//不按鼠标移动
glutMotionFunc(yourFunc1);//按鼠标移动
openGL两个鼠标事件返回的鼠标坐标都以左上角为原点,如下图,所以会导致最后实现的第一人称视角y轴反转,这个很容易解决,计算出的角度加一个负号或者处理一下屏幕坐标将原点变为左下角。
//不按鼠标移动
void onMouseMovePassive(int screen_x,int screen_y) {
//相对于屏幕中心点的偏移量
float offsetx = screen_x - centerpoint_x;
float offsety = screen_y - centerpoint_y;
//偏移量转化为角度
screenrate_x = offsetx / centerpoint_x *PI;//水平方向
screenrate_y = offsety / centerpoint_y *PI/2;//竖直方向
//水平方向
look_x_temp = r * sin(screenrate_x);
look_z_temp = r * cos(screenrate_x);//最后使用时要和相机坐标相加/减
//竖直方向
look_y = r * sin(-screenrate_y);
float r_castlenght = abs(r * cos(screenrate_y));//投影在xz面的长度
//根据长度计算真正的look_x,look_z
look_x = r_castlenght * look_x_temp / r;
look_z = r_castlenght * look_z_temp / r;
}
因为在水平方向最大要能转180度,而竖直方向最大转90度,所以鼠标相对中心点移动比例转换成角度时,水平方向要乘以180度,竖直方向乘以90度
(这个第一人称仍然有它的问题,比如鼠标移动到边框后不可继续移动。以往我们在FPS游戏中的视角都是只要鼠标在往右拖动就可以一直转向,但由于我们使用的是鼠标相对中心点的偏移,导致最大只能转向180度,将水平方向偏移角度的PI增大几倍可以一定程度缓解这个问题,但同时也会使得“灵敏度”过高,目前还没有想到一个好的解决办法)
因为gluLookAt参数的第一组和第二组点都是坐标,第一人称摄像机是可以移动的,我们要计算第二组摄像机看向的点,就要计算摄像机看向的点相对于摄像机的位置
计算球面坐标
先看水平方向,a为scrrenrate_x,通过屏幕坐标在水平方向上的偏移量计算出的角度,先从y轴俯视xoz平面
摄像机朝向为Z轴负方向
可以得到水平方向上相机看向的点的xz坐标为( r*sin(a),r*cos(a) ),r随便设一个数字即可
将Y竖直方向考虑进来,b即是screenrate_y,根据屏幕坐标在水平方向上偏移量计算出的角度,
y = r*sin(b)
半径r在xoz平面的投影长度r_castlength为abs(r*cos(b)),即原点到(x,o,z)点的距离,结合之前在水平方向上求出的点(x',0,z')
x' = r*sin(a),z' = r*cos(a) ,相似三角形就能求出x,z的值
得到球面坐标(x,y,z)是相对于相机的坐标
在gluLookAt函数中将相对于摄像机的坐标变为世界坐标系中的坐标
gluLookAt(cpos_x, cpos_y, cpos_z, cpos_x + look_x, look_y+look_y, cpos_z - look_z, 0, 1, 0);
其中cpos_xyz是摄像机的坐标,因为我的相机因为其他功能需求的原因,初始是面朝Z轴负方向,所以第6个参数是cpos_z - look_z。如果没有特殊需求,为了方便理解初始面还是面朝Z正方向比较不容易出错
相机移动就很简单了,glutKeyboardFunc设置键盘函数,根据上面计算xoz面的坐标来控制WASD往对应方向移动
因为Z轴反过来了所以想起来有些绕,为了保险还是在纸上自己画一画对照着做。
float step = 0.01;
void keyboardFunc(unsigned char key,int x,int y) {
if (key == 'w') {
//朝镜头方向前进look_x,look_z
cpos_x += look_x_temp * step;
cpos_z -= look_z_temp * step;
}
if (key == 's') {
//后退
cpos_x -= look_x_temp * step;
cpos_z += look_z_temp * step;
}
if (key == 'a') {
//向左移动 (x,y)对应(y,-x)
//则(look_x,look_z)对应的为(-look_z,look_x)
//Z轴取反
cpos_x += -look_z_temp * step;
cpos_z -= look_x_temp * step;
}
if (key == 'd') {
//向右移动 (x,y)对应(-y,x)
//则(look_x,look_z)对应的为(look_z,-look_x)
//Z轴取反
cpos_x += look_z_temp * step;
cpos_z -= -look_x_temp * step;
}
}
因为使用的是只考虑平面的look_x_temp,look_z_temp,而不是根据球面坐标计算的look_x,look_z,所以不会有抬头低头速度不一样、斜着走比正着走快(因为根本没法斜着走hhhhh)等问题
#define _CRT_SECURE_NO_WARNINGS
#define WindowWidth 400
#define WindowHeight 400
#define WindowTitle "第一人称视角漫游demo"
#include
#include
#include
#include
#include
using namespace std;
float PI = 3.14;
//视角控制
float look_x = 0,
look_y = 0,
look_z = 0,
look_x_temp = 0,
look_z_temp = 0;
float screenrate_x, screenrate_y;//鼠标屏幕坐标相对于中心点移动的比例
float r = 10;
//相机位置
float cpos_x = 0,
cpos_y = 2,
cpos_z = 10;
int centerpoint_x = WindowWidth / 2,
centerpoint_y = WindowHeight / 2;
void display(void)
{
// 清除屏幕
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 设置视角
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(75, 1, 1, 30);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(cpos_x, cpos_y, cpos_z, cpos_x + look_x, cpos_y + look_y, cpos_z - look_z, 0, 1, 0);//注意因为相机是面朝z负方向,所以cpos_z - lookz
//gluLookAt(cpos_x, cpos_y, cpos_z, 0, 0, 0, 0, 1, 0);//原固定视角
//蓝色地面
glColor3f(0.0, 0.0, 1.0);
glBegin(GL_QUADS);
glVertex3f(-8.0f, 0.0f, -8.0f);
glVertex3f(-8.0f, 0.0f, 8.0f);
glVertex3f(8.0f, 0.0f, 8.0f);
glVertex3f(8.0f, 0.0f, -8.0f);
glEnd();
//红色墙面
glColor3f(1.0, 0.0, 0.0);
glBegin(GL_QUADS);
glVertex3f(8.0f, 0.0f, -8.0f);
glVertex3f(8.0f, 8.0f, -8.0f);
glVertex3f(-8.0f, 8.0f, -8.0f);
glVertex3f(-8.0f, 0.0f, -8.0f);
glEnd();
glutSwapBuffers();
}
void myIdle(void)
{
display();
}
//不按鼠标移动事件
void onMouseMovePassive(int screen_x, int screen_y) {
float offsetx = screen_x - centerpoint_x;
float offsety = screen_y - centerpoint_y;
screenrate_x = offsetx / centerpoint_x * PI;//用于摄像机水平移动
screenrate_y = offsety / centerpoint_y * PI / 2;//用于摄像机上下移动
//水平方向
look_x_temp = r * sin(screenrate_x);
look_z_temp = r * cos(screenrate_x);//最后使用时要和相机坐标相加/减
//竖直方向
look_y = r * sin(-screenrate_y);
float r_castlenght = abs(r * cos(screenrate_y));//投影在xz面的长度
//根据长度计算真正的look_x,look_z
look_x = r_castlenght * look_x_temp / r;
look_z = r_castlenght * look_z_temp / r;
}
//按下鼠标移动事件
void onMouseDownAndMove(int x, int y) {
}
float step = 0.01;
void keyboardFunc(unsigned char key, int x, int y) {
if (key == 'w') {
//朝镜头方向前进look_x,look_z
cpos_x += look_x_temp * step;
cpos_z -= look_z_temp * step;
}
if (key == 's') {
cpos_x -= look_x_temp * step;
cpos_z += look_z_temp * step;
}
if (key == 'a') {
//左 (x,y)对应(y,-x)
//则(look_x,look_z)对应的为(-look_z,look_x)
//Z轴取反
cpos_x += -look_z_temp * step;
cpos_z -= look_x_temp * step;
}
if (key == 'd') {
//右 (x,y)对应(-y,x)
//则(look_x,look_z)对应的为(look_z,-look_x)
//Z轴取反
cpos_x += look_z_temp * step;
cpos_z -= -look_x_temp * step;
}
}
int main(int argc, char* argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100, 100);
glutInitWindowSize(WindowWidth, WindowHeight);
glutCreateWindow(WindowTitle);
glutPassiveMotionFunc(onMouseMovePassive);
glutMotionFunc(onMouseDownAndMove);
glutKeyboardFunc(keyboardFunc);
glutDisplayFunc(&display);
glutIdleFunc(&myIdle);
glutMainLoop();
return 0;
}