openGL实现第一人称视角

最近做的一个题目要求用openGL实现一个漫游功能,虽然不知道这个漫游是不是指第一人称(其实我觉得第三人称俯视的那种也算),不过都差不多

主要使用openGL的gluLookAt函数,通过计算球面坐标来实现


目录

gluLookAt()

实现过程

demo

最终效果

gluLookAt()

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)等问题

demo

#define _CRT_SECURE_NO_WARNINGS
#define WindowWidth  400
#define WindowHeight 400
#define WindowTitle  "第一人称视角漫游demo"

#include <glut.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include<iostream>
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;
}

最终效果

  • 15
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值