[OpenGL] 球坐标系实现模型旋转效果

使用OpenGL实现了一个将三维模型可视化显示出来的软件,效果类似soliwork、catia等三维建模软件的效果,如下图。
[OpenGL] 球坐标系实现模型旋转效果_第1张图片
与各三维建模软件类似,需要实现一个旋转360°视角观察物体的操作,如下动图。

调查了一下三维建模软件实现类似功能的办法,大致是摄像机位置固定不动,给模型一个旋转矩阵,在世界坐标系内改变模型相对摄像机的朝向,从而达到旋转效果。
[OpenGL] 球坐标系实现模型旋转效果_第2张图片
这个方法可行,但很显然,它会改变三维模型内各顶点在世界坐标系上的坐标(因为旋转矩阵作用在了物体上)

由于项目需要,我不能随便改变三维模型内各顶点在世界坐标系上的坐标,即某个物体放置在世界坐标系后,尽量不能让任何旋转矩阵或平移矩阵作用在物体上。无奈,只能尝试采用别的方法实现。

我通过移动摄像机来达到与旋转物体同样的效果,使摄像机在一个球坐标系上移动,摄像机始终对准位于球坐标系原点处的物体。接下来介绍详细的实现方法。

先上代码。

#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

一、球坐标系

[OpenGL] 球坐标系实现模型旋转效果_第3张图片
定义如图的球坐标系,用球坐标表示摄像机位置 Position(P) 在三维空间中的位置,球坐标三个参数如下:

天顶角Zenith:向量OPy轴 正方向的夹角,大小(0°~360°)

方位角Azimuth:向量OPxoy平面上的投影与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=Rsin(Zenith)sin(Azimuth)

y = R ⋅ s i n ( Z e n i t h ) y=R \cdot sin(Zenith) y=Rsin(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=Rsin(Zenith)cos(Azimuth)

每次鼠标移动,只改变天顶角Zenith与方位角Azimuth,摄像机的位置P的直角坐标系坐标通过上述转换关系得到,这样一来,就能保证摄像机只在球表面运动。再将摄像机朝向始终对准球心,而物体就放置在球心处,就能达到旋转物体的效果了。

二、具体实现代码与解析

1、定义的摄像机类内存有属性

其中:

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;
}

2、通过鼠标移动改变天顶角与方位角

限定天顶角与方位角在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的加减,这样才能实现类似三维建模软件内所具有的,由鼠标控制旋转物体的视觉效果(当然,实际上物体并没有被旋转,旋转的是摄像机,这才是本文的意义)

3、更新Position变量

通过球坐标系与直角坐标系的转换关系,求得改变位置后的摄像机的直角坐标系坐标,更新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::singlm::cos等三角函数的输入形式是弧度,因此这里需要使用glm::radians将角度转换成弧度。

4、更新摄像机的三个方位向量(Front、Right、Up)

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°这两个特殊角度附近时,图像会产生一个左右突变的现象,如下动图所示。


偶然发现某个游戏《群星》内也存在这种突变现象,估计就是同样的原因引起的。

你可能感兴趣的:(OpenGL,c++,计算机视觉,交互,3d)