本节讨论如何在OpenGL中配置一个FPS风格的摄像机,让你能够在3D场景中自由移动。
摄像机/观察空间(Camera/View Space)
1. 摄像机位置
获取摄像机位置很简单。摄像机位置简单来说就是世界空间中一个指向摄像机位置的向量。
不要忘记正z轴是从屏幕指向你的,如果我们希望摄像机向后移动,我们就沿着z轴的正方向移动。glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
2. 摄像机方向
摄像机的方向是它指向哪个方向。现在我们让摄像机指向场景原点:(0, 0, 0)。
“方向向量”是指向z轴正方向的,而不是摄像机所注视的那个方向.glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraDirection = glm::normalize(cameraPos - cameraTarget);
3. 右轴
右向量(Right Vector),它代表摄像机空间的x轴的正方向。为获取右向量我们需要先使用一个小技巧:先定义一个上向量(Up Vector)。接下来把上向量和第二步得到的方向向量进行叉乘。两个向量叉乘的结果会同时垂直于两向量,因此我们会得到指向x轴正方向的那个向量
glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f);
glm::vec3 cameraRight = glm::normalize(glm::cross(up, cameraDirection));
4. 上轴
现在我们已经有了x轴向量和z轴向量,获取一个指向摄像机的正y轴向量就相对简单了:我们把右向量和方向向量进行叉乘:
glm::vec3 cameraUp = glm::cross(cameraDirection, cameraRight);
Look At 矩阵
以下我们来做些有意思的事,把我们的摄像机在场景中旋转。我们会将摄像机的注视点保持在(0, 0, 0)。
我们需要用到一点三角学的知识来在每一帧创建一个x和z坐标,它会代表圆上的一点,我们将会使用它作为摄像机的位置。通过重新计算x和y坐标,我们会遍历圆上的所有点,这样摄像机就会绕着场景旋转了。
//(1)摄像机旋转
camAngle++;
float radius = 10;
float camX = sinf(CC_DEGREES_TO_RADIANS(camAngle)) * radius;
float camZ = cosf(CC_DEGREES_TO_RADIANS(camAngle)) * radius;
Mat4::createLookAt(Vec3(camX, 0, camZ), Vec3::ZERO, Vec3(0, 1, 0), myCamera);
摄像机矩阵传入 vertex shader:
//摄像机矩阵传入 vertex shader
GLuint viewLoc = glGetUniformLocation(glprogram->getProgram(), "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, myCamera->m);
摄像机自由移动
camPos = Vec3(0, 0, 3);
camFront = Vec3(0, 0, -1);
camUp = Vec3(0, 1, 0);
Mat4::createLookAt(camPos, camPos + camFront, camUp, myCamera);
首先将摄像机位置设置为之前定义的
cameraPos
。方向是当前的位置加上我们刚刚定义的方向向量。这样能保证无论我们怎么移动,摄像机都会注视着目标方向。让我们摆弄一下这些向量,在按下某些按钮时更新
cameraPos
向量。
void OpenGLCamera::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
Vec2 curpos = touch->getLocationInView();
Vec2 prepos = touch->getPreviousLocationInView();
GLfloat dx = curpos.x - prepos.x;
GLfloat dy = curpos.y - prepos.y;
//移动摄像机
GLfloat camspeed = 0.05f;
if (curpos.y - prepos.y > 0) { //w
camPos += camspeed * camFront;
}else if (curpos.y - prepos.y < 0){ //s
camPos -= camspeed * camFront;
}
if (curpos.x - prepos.x < 0){ //a
Vec3 right = Vec3::ZERO;
Vec3::cross(camFront, camUp, &right);
right.normalize();
camPos -= right * camspeed;
}else if (curpos.x - prepos.x > 0){ //d
Vec3 right = Vec3::ZERO;
Vec3::cross(camFront, camUp, &right);
right.normalize();
camPos += right * camspeed;
}
}
视角移动
欧拉角
欧拉角(Euler Angle)是可以表示3D空间中任何旋转的3个值,由莱昂哈德·欧拉(Leonhard Euler)在18世纪提出。一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:
俯仰角是描述我们如何往上或往下看的角,可以在第一张图中看到。第二张图展示了偏航角,偏航角表示我们往左和往右看的程度。滚转角代表我们如何翻滚摄像机,通常在太空飞船的摄像机中使用。每个欧拉角都有一个值来表示,把三个角结合起来我们就能够计算3D空间中任何的旋转向量了。
给定一个俯仰角和偏航角,我们可以把它们转换为一个代表新的方向向量的3D向量。俯仰角和偏航角转换为方向向量的处理需要一些三角学知识,我们先从最基本的情况开始:
如果我们把斜边边长定义为1,我们就能知道邻边的长度是 cos x/h=cos x/1=cos x ,它的对边是 sin y/h=sin y/1=sin y 。这样我们获得了能够得到x和y方向长度的通用公式,它们取决于所给的角度。我们使用它来计算方向向量的分量:
这个三角形看起来和前面的三角形很像,所以如果我们想象自己在xz平面上,看向y轴,我们可以基于第一个三角形计算来计算它的长度/y方向的强度(Strength)(我们往上或往下看多少)。从图中我们可以看到对于一个给定俯仰角的y值等于 sin θ :
direction.y = sin(glm::radians(pitch)); // 注意我们先把角度转为弧度
这里我们只更新了y值,仔细观察x和z分量也被影响了。从三角形中我们可以看到它们的值等于:
direction.x = cos(glm::radians(pitch));
direction.z = cos(glm::radians(pitch));
为偏航角找到需要的分量:
就像俯仰角的三角形一样,我们可以看到x分量取决于cos(yaw)
的值,z值同样取决于偏航角的正弦值。把这个加到前面的值中,会得到基于俯仰角和偏航角的方向向量:
direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); // 译注:direction代表摄像机的前轴(Front),这个前轴是和本文第一幅图片的第二个摄像机的方向向量是相反的
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));
touch 输入
void OpenGLCamera::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
Vec2 curpos = touch->getLocationInView();
Vec2 prepos = touch->getPreviousLocationInView();
GLfloat dx = curpos.x - prepos.x;
GLfloat dy = curpos.y - prepos.y;
//(3)旋转摄像机
pitch += dy * camspeed; //绕x轴,俯仰角
if(pitch > 89.0f){
pitch = 89.0f;
}
if(pitch < -89.0f){
pitch = -89.0f;
}
yaw += dx * camspeed; //绕y轴,偏航角
GLfloat pitch_radian = CC_DEGREES_TO_RADIANS(pitch);
GLfloat yaw_radian = CC_DEGREES_TO_RADIANS(yaw);
Vec3 dir = Vec3(cosf(pitch_radian) * cosf(yaw_radian), sinf(pitch_radian), cosf(pitch_radian) * sinf(yaw_radian));
dir.normalize();
camFront = dir;
}
我们需要给摄像机添加一些限制,这样摄像机就不会发生奇怪的移动了(这样也会避免一些奇怪的问题)。对于俯仰角,要让用户不能看向高于89度的地方(在90度时视角会发生逆转,所以我们把89度作为极限),同样也不允许小于-89度。这样能够保证用户只能看到天空或脚下,但是不能超越这个限制。
缩放(Zoom)
void OpenGLCamera::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
Vec2 curpos = touch->getLocationInView();
Vec2 prepos = touch->getPreviousLocationInView();
GLfloat dx = curpos.x - prepos.x;
GLfloat dy = curpos.y - prepos.y;
GLfloat camspeed = 0.05f;
//(4)缩放
if(fov >= 1.0f && fov <= 45.0f){
fov -= dx * camspeed;
}
if(fov <= 1.0f){
fov = 1.0f;
}
if(fov >= 45.0f){
fov = 45.0f;
}
}
现在在每一帧都必须把透视投影矩阵上传到GPU,但现在使用
fov
变量作为它的视野:
Mat4::createPerspective(fov, viewsize.width/viewsize.height, 0.1, 200, projection);
//
// Triangle.cpp
// shaderTest
//
// Created by MacSBL on 2016/12/15.
//
//
#include "OpenGLCamera.h"
bool OpenGLCamera::init()
{
if (!Layer::init()) {
return false;
}
GLfloat vertices[] = {
//3维顶点, //纹理坐标
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
//创建并绑定纹理
//
//方法1
auto sprite = Sprite::create("awesomeface.png");
textureId2 = sprite->getTexture()->getName();
//
//方法2
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
//纹理环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//纹理过滤
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//加载纹理
Image* img = new Image();
img->initWithImageFile("wall.jpg");
int width = img->getWidth();
int height = img->getHeight();
unsigned char* imgdata = img->getData();
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
glGenerateMipmap(GL_TEXTURE_2D);
//释放内存,解绑texture
CC_SAFE_DELETE(img);
glBindTexture(GL_TEXTURE_2D, 0);
///
//1、绑定vao
//顶点数组对象(Vertex Array Object)被绑定后,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// 2. 把顶点数组复制到缓冲(vbo)中供OpenGL使用
//使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
//把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//解析顶点数据, 应用到第1个顶点属性上
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5* sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 纹理坐标,应用到第2个属性上
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);
//解除绑定vao和vbo
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
auto program = new GLProgram;
program->initWithFilenames("cube.vsh", "cube.fsh");
program->link();
this->setGLProgram(program);
//——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
//矩阵
//绕某轴旋转x°
model = new Mat4();
model->rotate(Vec3(1, 0, 0), CC_DEGREES_TO_RADIANS(50));
//离我们有一些距离
view = new Mat4();
view->translate(0, 0, -3);
//有透视效果(顶点越远,变得越小)
cocos2d::Size viewsize = Director::getInstance()->getVisibleSize();
projection = new Mat4();
Mat4::createPerspective(45, viewsize.width/viewsize.height, 0.1, 200, projection);
//摄像机
//(1), 旋转
myCamera = new Mat4();
//(2),自由移动,通过touch methods
camPos = Vec3(0, 0, 3);
camFront = Vec3(0, 0, -1);
camUp = Vec3(0, 1, 0);
Mat4::createLookAt(camPos, camPos + camFront, camUp, myCamera);
//开启深度测试
Director::getInstance()->setDepthTest(true);
cubpos[0] = {0, 0, 0};
cubpos[1] = {2, 5, -15};
cubpos[2] = {-1, -2, -2};
cubpos[3] = {1, 0.5, -1.6};
//touch事件
auto elistener = EventListenerTouchOneByOne::create();
elistener->onTouchBegan = CC_CALLBACK_2(OpenGLCamera::onTouchBegan, this);
elistener->onTouchMoved = CC_CALLBACK_2(OpenGLCamera::onTouchMoved, this);
elistener->onTouchEnded = CC_CALLBACK_2(OpenGLCamera::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(elistener, this);
return true;
}
void OpenGLCamera::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
{
Layer::visit(renderer, parentTransform, parentFlags);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLCamera::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLCamera::onDraw()
{
//获取当前node 的shader
auto glprogram = getGLProgram();
//需要在init中指定shader才能在这use
glprogram->use();
//绑定纹理, 它会自动把纹理赋值给片段着色器texture.fsh的采样器u_myTexture.因为一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
// GL::bindTexture2D(textureId);
/
//纹理单元, uniform采样器对应纹理单元
GL::bindTexture2DN(0, textureId);
glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture1"), 0);
GL::bindTexture2DN(1, textureId2);
glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture2"), 1);
/
//矩阵变换,给vertex shader传值
// model->rotate(Vec3(1, 0, 0), CC_DEGREES_TO_RADIANS(2));
// GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
// glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model->m);
//(1)摄像机旋转
// camAngle++;
// float radius = 10;
// float camX = sinf(CC_DEGREES_TO_RADIANS(camAngle)) * radius;
// float camZ = cosf(CC_DEGREES_TO_RADIANS(camAngle)) * radius;
// Mat4::createLookAt(Vec3(camX, 0, camZ), Vec3::ZERO, Vec3(0, 1, 0), myCamera);
//(2) touch move 摄像机
Mat4::createLookAt(camPos, camPos + camFront, camUp, myCamera);
//摄像机矩阵传入 vertex shader
GLuint viewLoc = glGetUniformLocation(glprogram->getProgram(), "view");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, myCamera->m);
//(4)摄像机缩放
cocos2d::Size viewsize = Director::getInstance()->getVisibleSize();
Mat4::createPerspective(fov, viewsize.width/viewsize.height, 0.1, 200, projection);
GLuint projectionLoc = glGetUniformLocation(glprogram->getProgram(), "projection");
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, projection->m);
glBindVertexArray(vao);
for (int i=0; i<4; i++) { //画n个立方体
Mat4* posmodel = new Mat4();
posmodel->translate(cubpos[i]);
posmodel->rotate(Vec3(0, 1, 0), CC_DEGREES_TO_RADIANS(50*i));
GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, posmodel->m);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
glBindVertexArray(0);
}
#pragma mark touch
bool OpenGLCamera::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *evt)
{
return true;
}
void OpenGLCamera::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
Vec2 curpos = touch->getLocationInView();
Vec2 prepos = touch->getPreviousLocationInView();
GLfloat dx = curpos.x - prepos.x;
GLfloat dy = curpos.y - prepos.y;
//移动摄像机
GLfloat camspeed = 0.05f;
// if (curpos.y - prepos.y > 0) { //w
// camPos += camspeed * camFront;
// }else if (curpos.y - prepos.y < 0){ //s
// camPos -= camspeed * camFront;
// }
// if (curpos.x - prepos.x < 0){ //a
// Vec3 right = Vec3::ZERO;
// Vec3::cross(camFront, camUp, &right);
// right.normalize();
// camPos -= right * camspeed;
// }else if (curpos.x - prepos.x > 0){ //d
// Vec3 right = Vec3::ZERO;
// Vec3::cross(camFront, camUp, &right);
// right.normalize();
// camPos += right * camspeed;
// }
//(3)旋转摄像机
// pitch += dy * camspeed; //绕x轴,俯仰角
// if(pitch > 89.0f){
// pitch = 89.0f;
// }
// if(pitch < -89.0f){
// pitch = -89.0f;
// }
// yaw += dx * camspeed; //绕y轴,偏航角
// GLfloat pitch_radian = CC_DEGREES_TO_RADIANS(pitch);
// GLfloat yaw_radian = CC_DEGREES_TO_RADIANS(yaw);
// Vec3 dir = Vec3(cosf(pitch_radian) * cosf(yaw_radian), sinf(pitch_radian), cosf(pitch_radian) * sinf(yaw_radian));
// dir.normalize();
// camFront = dir;
//(4)缩放
if(fov >= 1.0f && fov <= 45.0f){
fov -= dx * camspeed;
}
if(fov <= 1.0f){
fov = 1.0f;
}
if(fov >= 45.0f){
fov = 45.0f;
}
}
void OpenGLCamera::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *evt)
{
}