使用OpenGL实现了一个将三维模型可视化显示出来的软件,效果类似soliwork、catia等三维建模软件的效果,如下图。
与各三维建模软件类似,需要实现一个旋转360°视角观察物体的操作,如下动图。
调查了一下三维建模软件实现类似功能的办法,大致是摄像机位置固定不动,给模型一个旋转矩阵,在世界坐标系内改变模型相对摄像机的朝向,从而达到旋转效果。
这个方法可行,但很显然,它会改变三维模型内各顶点在世界坐标系上的坐标(因为旋转矩阵作用在了物体上)
由于项目需要,我不能随便改变三维模型内各顶点在世界坐标系上的坐标,即某个物体放置在世界坐标系后,尽量不能让任何旋转矩阵或平移矩阵作用在物体上。无奈,只能尝试采用别的方法实现。
我通过移动摄像机来达到与旋转物体同样的效果,使摄像机在一个球坐标系上移动,摄像机始终对准位于球坐标系原点处的物体。接下来介绍详细的实现方法。
先上代码。
#pragma once
#ifndef SPHERECAMERA_H
#define SPHERECAMERA_H
#include
#include
#include
#include
//#include
//用枚举类型定义几个可能的摄像机运动方向
enum Camera_Movement {
FORWARD,//0
BACKWARD,//1
LEFT,//2
RIGHT,//3
UP,//4
DOWN//5
};
//定义默认的摄像机数值
//偏航角
const float YAW = -90.0f;
//俯仰角
const float PITCH = 0.0f;
//移动速度
const float SPEED = 15.0f;
//灵敏度
const float SENSITIVITY = 0.01f;
//缩放
const float ZOOM = 45.0f;
//摄像机类
class SphereCamera
{
//属性
public:
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
//第一人称
//float Yaw;
//float Pitch;
//球坐标系
//天顶角,角度
float Zenith;
//方位角
float Azimuth;
float R;
float MovementSpeed;
float MouseSensitivity;
float Zoom;
//构造函数(用向量构造)
SphereCamera(glm::vec3 position, glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f)) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM)
{
Position = position;
WorldUp = up;
R = glm::length(glm::vec3(this->Position.x, this->Position.y, this->Position.z));
Zenith = glm::degrees(acos(this->Position.y / R));
Azimuth = glm::degrees(atan(this->Position.x / this->Position.z));
updataCameraVectors();
}
//方法
//计算用lookAt得出的视图矩阵
glm::mat4 GetViewMatrix()
{
return glm::lookAt(Position, Position + Front, Up);
}
//移动摄像机
void CameraMove(Camera_Movement direction, float deltaTime)
{
//速度
float velocity = MovementSpeed * deltaTime;
//前后左右上下
if (direction == FORWARD)
Position += Front * velocity;
if (direction == BACKWARD)
Position -= Front * velocity;
if (direction == LEFT)
Position -= Right * velocity;
if (direction == RIGHT)
Position += Right * velocity;
if (direction == UP)
Position += Up * velocity;
if (direction == DOWN)
Position -= Up * velocity;
//改变完摄像机位置后需要重新计算球坐标系的半径参数R
this->R = glm::length(glm::vec3(this->Position.x, this->Position.y, this->Position.z));
std::cout << "(" << this->Position.x << "," << this->Position.y << "," << this->Position.z << ")" << std::endl;
}
//鼠标旋转摄像机视角
void ProcessMouseRotate(float xoffset, float yoffset)
{
//xoffset *= MouseSensitivity;
//yoffset *= MouseSensitivity;
//限定方位角和天顶角变化时的循环过程
//这一步在+-offset前做会在天顶角由0最后一减时出现一瞬间的图像突变,但下一帧就回复正常
if (Azimuth < 0)Azimuth = 360 - abs(Azimuth);
else Azimuth = (int)Azimuth % 360;
if (Zenith < 0)Zenith = 360 - abs(Zenith);
else Zenith = (int)Zenith % 360;
//由天顶角是否大于180可知摄像机此时的姿态,决定鼠标的移动如何增减角度
if (Zenith > 180)
{
Azimuth += xoffset;
Zenith += yoffset;
}
else
{
Azimuth -= xoffset;
Zenith += yoffset;
}
//std::cout << "xoffset:" << xoffset << std::endl;
//std::cout << "yoffset:" << yoffset << std::endl;
//std::cout << "Azimuth:" << Azimuth << std::endl;
//std::cout << "Zenith:" << Zenith << std::endl;
this->Position.x = this->R * glm::sin(glm::radians(Zenith)) * glm::sin(glm::radians(Azimuth));
this->Position.y = this->R * glm::cos(glm::radians(Zenith));
this->Position.z = this->R * glm::sin(glm::radians(Zenith)) * glm::cos(glm::radians(Azimuth));
//std::cout << "(" << this->Position.x << "," << this->Position.y << "," << this->Position.z << ")" << std::endl;
updataCameraVectors();
}
//鼠标移动摄像机视角
void ProcessMouseMovement(float xoffset, float yoffset, float deltaTime)
{
}
private:
//计算摄像机的front vector
void updataCameraVectors()
{
glm::vec3 front;
//第一人称
//front.x = cos(glm::radians(Yaw))*cos(glm::radians(Pitch));
//front.y = sin(glm::radians(Pitch));
//front.z = sin(glm::radians(Yaw))*cos(glm::radians(Pitch));
//第三人称球坐标系摄像机
//摄像机始终看向世界坐标系原点方向
front.x = -(this->Position.x);
front.y = -(this->Position.y);
front.z = -(this->Position.z);
Front = glm::normalize(front);
//重新计算Right 和 Up vector
//当天顶角>180时,摄像机上方向需取反,否则画面会产生突变
if (Zenith > 180)
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = -glm::normalize(glm::cross(Right, Front));
}
else
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}
}
};
#endif
定义如图的球坐标系,用球坐标表示摄像机位置 Position(P) 在三维空间中的位置,球坐标三个参数如下:
天顶角Zenith:向量OP与 y轴 正方向的夹角,大小(0°~360°)
方位角Azimuth:向量OP在xoy平面上的投影与z轴正方向的夹角,大小(0°~360°)
半径R:向量OP的长度
注意,此球坐标系与常见的球坐标系的定义略有不同。
球坐标系与直角坐标系的转换关系如下:
x = R ⋅ s i n ( Z e n i t h ) ⋅ s i n ( A z i m u t h ) x=R \cdot sin(Zenith) \cdot sin(Azimuth) x=R⋅sin(Zenith)⋅sin(Azimuth)
y = R ⋅ s i n ( Z e n i t h ) y=R \cdot sin(Zenith) y=R⋅sin(Zenith)
z = R ⋅ s i n ( Z e n i t h ) ⋅ c o s ( A z i m u t h ) z=R \cdot sin(Zenith) \cdot cos(Azimuth) z=R⋅sin(Zenith)⋅cos(Azimuth)
每次鼠标移动,只改变天顶角Zenith与方位角Azimuth,摄像机的位置P的直角坐标系坐标通过上述转换关系得到,这样一来,就能保证摄像机只在球表面运动。再将摄像机朝向始终对准球心,而物体就放置在球心处,就能达到旋转物体的效果了。
其中:
Position为摄像机位置,即摄像机在世界坐标系上的坐标
Front、Up、Right为OpenGL内摄像机三个方位向量
WorldUp为世界上向量
Zenith为天顶角
Azimuth为方位角
R为半径
//摄像机类
class SphereCamera
{
//属性
public:
glm::vec3 Position;
glm::vec3 Front;
glm::vec3 Up;
glm::vec3 Right;
glm::vec3 WorldUp;
//球坐标系
//天顶角,角度
float Zenith;
//方位角
float Azimuth;
//半径
float R;
}
限定天顶角与方位角在0°~360°的范围内循环
//限定方位角和天顶角变化时的循环过程
//这一步在+-offset前做会在天顶角由0最后一减时出现一瞬间的图像突变,但下一帧就回复正常
if (Azimuth < 0)Azimuth = 360 - abs(Azimuth);
else Azimuth = (int)Azimuth % 360;
if (Zenith < 0)Zenith = 360 - abs(Zenith);
else Zenith = (int)Zenith % 360;
通过鼠标在屏幕上的横坐标偏移量xoffset和纵坐标偏移量yoffset,加减天顶角与方位角的值
if (Zenith > 180)
{
Azimuth += xoffset;
Zenith += yoffset;
}
else
{
Azimuth -= xoffset;
Zenith += yoffset;
}
注意:在天顶角Zenith位于 0°~180° 和 180°~360°两个半球时,鼠标横坐标偏移量xoffset意味着相反的方位角Azimuth的加减,这样才能实现类似三维建模软件内所具有的,由鼠标控制旋转物体的视觉效果(当然,实际上物体并没有被旋转,旋转的是摄像机,这才是本文的意义)
通过球坐标系与直角坐标系的转换关系,求得改变位置后的摄像机的直角坐标系坐标,更新Position变量
this->Position.x = this->R * glm::sin(glm::radians(Zenith)) * glm::sin(glm::radians(Azimuth));
this->Position.y = this->R * glm::cos(glm::radians(Zenith));
this->Position.z = this->R * glm::sin(glm::radians(Zenith)) * glm::cos(glm::radians(Azimuth));
注意:在定义摄像机类的向量时,两个角(天顶角与方位角)是角度,在2步骤内对两个角的加减也是角度形式。而glm::sin 与 glm::cos等三角函数的输入形式是弧度,因此这里需要使用glm::radians将角度转换成弧度。
updataCameraVectors();
此处调用的是类内定义的方法:
private:
//计算摄像机的front vector
void updataCameraVectors()
{
glm::vec3 front;
//第三人称球坐标系摄像机
//摄像机始终看向世界坐标系原点方向
front.x = -(this->Position.x);
front.y = -(this->Position.y);
front.z = -(this->Position.z);
Front = glm::normalize(front);
//重新计算Right 和 Up vector
//当天顶角>180时,摄像机上方向需取反,否则画面会产生突变
if (Zenith > 180)
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = -glm::normalize(glm::cross(Right, Front));
}
else
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}
}
front.x = -(this->Position.x);
front.y = -(this->Position.y);
front.z = -(this->Position.z);
Front = glm::normalize(front);
这里保证了摄像机的Front向量始终指向世界坐标系原点(本文范围内,物体坐标系与世界坐标系重合,即物体就放置在世界坐标系原点处)
注意到,在更新摄像机的三个方位变量的方法内,同样需要将情况分成两个半球考虑
if (Zenith > 180)
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = -glm::normalize(glm::cross(Right, Front));
}
else
{
Right = glm::normalize(glm::cross(Front, WorldUp));
Up = glm::normalize(glm::cross(Right, Front));
}
这是因为,如果不这么做的话,摄像机的天顶角Zenith在变动到0°和180°这两个特殊角度附近时,图像会产生一个左右突变的现象,如下动图所示。
偶然发现某个游戏《群星》内也存在这种突变现象,估计就是同样的原因引起的。