在一个场景中最重要的就是和用户的交互,所以实现一个摄像机对整个程序的影响是很大的,本文旨在实现比较实用的摄像机。主要包括的功能为:按下鼠标左键对整个场景进行移动,按下鼠标右键对整个场景进行旋转操作,根据鼠标位置滚动滚轮进行视图的放大缩小。
摄像机类
在OpenGL中定义一个摄像机坐标系需要有三个元素,摄像机的位置,摄像机的视点,摄像机向上的向量,又这三个元素即可算出摄像机矩阵,但是在我们的摄像机中需要管理投影矩阵和模型矩阵,所以定义了以下变量。
real3 _eye;
real3 _up;
real3 _right;
real3 _target;
real3 _dir;
matrix4r _matView;
matrix4r _matProj;
matrix4r _matWorld;
real2 _viewSize;
real3 _oldLength;
这是他的初始化
CELLCamera(const real3& target = real3(0,0,0),const real3& eye = real3(0,100,100),const real3& right = real3(1,0,0))
{
_viewSize = real2(256,256);
_matView = CELL::matrix4r(1);
_matProj = CELL::matrix4r(1);
_matWorld = CELL::matrix4r(1);
_oldLength = 10;
_target = target;
_eye = eye;
_dir = normalize(_target - _eye);
_right = right;
_up = normalize(cross(_right,_dir));
}
~CELLCamera()
{}
其中定义了一些重要的函数我来解释一下。
首先是将世界坐标转化为窗口坐标
/**
* 世界坐标转化为窗口坐标
*/
bool project( const real4& world, real4& screen )
{
screen = (_matProj * _matView * _matWorld) * world;
if (screen.w == 0.0f)
{
return false;
}
screen.x /= screen.w;
screen.y /= screen.w;
screen.z /= screen.w;
// map to range 0 - 1
screen.x = screen.x * 0.5f + 0.5f;
screen.y = screen.y * 0.5f + 0.5f;
screen.z = screen.z * 0.5f + 0.5f;
// map to viewport
screen.x = screen.x * _viewSize.x;
screen.y = _viewSize.y - (screen.y * _viewSize.y);
return true;
}
我们都知道一般我们每个模型都有对应自己的坐标,我们需要经过坐标变换后才能显示在对应的OpenGL的窗口中,这部分可以参考一下ai的解释
1.坐标变换流程概览
世界坐标到屏幕坐标的转换需要经过以下5个阶段的矩阵变换:
世界坐标 → 观察坐标 → 裁剪坐标 → 标准化设备坐标(NDC) → 屏幕坐标
每个阶段对应不同的矩阵操作和空间映射。
2. 具体转换步骤
(1) 模型视图变换(世界坐标 → 观察坐标)
作用:将物体从世界坐标系转换到相机视角坐标系。
矩阵操作:
模型矩阵(Model Matrix):处理物体的平移、旋转、缩放(世界空间变换)。
视图矩阵(View Matrix):通过gluLookAt或手动设置相机位置、观察点和上方向量,定义观察者的视角。
(2) 投影变换(观察坐标 → 裁剪坐标)
作用:将3D场景投影到2D视平面上,并确定可见范围。
投影类型:
透视投影(glm::perspective):模拟人眼视角,近大远小,适合3D场景。
正交投影(glm::ortho):保持物体尺寸不变,适合2D UI或CAD工具。
(3) 透视除法(裁剪坐标 → 标准化设备坐标)
作用:将齐次坐标转换为归一化设备坐标(NDC),范围[-1, 1]。
超出[-1,1]范围的坐标将被裁剪。
(4) 视口变换(NDC → 屏幕坐标)
作用:将NDC坐标映射到实际屏幕像素位置。
参数设置:
通过glViewport(x, y, width, height)定义视口区域。
Ray createRayFromScreen(int x,int y)
{
real4 minWorld;
real4 maxWorld;
real4 screen(real(x),real(y),0,1);
real4 screen1(real(x),real(y),1,1);
unProject(screen,minWorld);
unProject(screen1,maxWorld);
Ray ray;
ray.setOrigin(real3(minWorld.x,minWorld.y,minWorld.z));
real3 dir(maxWorld.x - minWorld.x,maxWorld.y - minWorld.y, maxWorld.z - minWorld.z);
ray.setDirection(normalize(dir));
return ray;
}
根据鼠标位置创建一条射线。
鼠标左击
鼠标左击实现的是鼠标左键按下拖动的时候场景进行平移,主要的实现思想是记录下当前的鼠标位置和上一次鼠标的位置,将摄像机的位置和摄像机的视点位置改变就行。
首先根据鼠标点击的位置求出与场景的交点,再根据这个交点和原位置得出一个差改变位置即可。
首先是鼠标按下事件
if (id == CELL::MouseButton::Left)
{
Ray ray = _camera.createRayFromScreen(absx,absy);
float3 pos = ray.getOrigin();
float tm = abs((pos.y - 0) / ray.getDirection().y);
float3 target = ray.getPoint(tm);
_role.setTarget(float3(target.x,0,target.z));
_leftButtonDown = true;
_ptMouseDown = int2(absx, absy);
}
首先根据鼠标的位置算出一条射线,然后根据这条射线计算出与场景的交点并记录,在鼠标移动的时候改变摄像机的位置和视点位置。
if (_leftButtonDown)
{
int2 pos(absx, absy);
/**
* 首先计算出来一个像素和当前场景的比例
*/
Ray ray0 = _camera.createRayFromScreen(pos.x, pos.y);
Ray ray1 = _camera.createRayFromScreen(_ptMouseDown.x, _ptMouseDown.y);
real3 pos0 = calcIntersectPoint(ray0);
real3 pos1 = calcIntersectPoint(ray1);
real3 offset = pos1 - pos0;
if (offset.x == 0 && offset.y == 0 && offset.z == 0)
{
//offset = float3(0.1f, 0, 0.1f);
}
_ptMouseDown = pos;
real3 newEye = _camera.getEye() + offset;
real3 newTgt = _camera.getTarget() + offset;
_camera.setEye(newEye);
_camera.setTarget(newTgt);
_camera.update();
}
鼠标右击
鼠标右击和鼠标左击也是一样的,都是获取到与场景的交点后改变视图矩阵。
鼠标按下事件
else if( id== CELL::MouseButton::Right)
{
_mousePos = float2(absx,absy);
_rightButtonDown = true;
/// 计算和地面的交点
Ray ray = _camera.createRayFromScreen(absx,absy);
float3 pos = ray.getOrigin();
float tm = abs((pos.y - 0) / ray.getDirection().y);
_mouseRot = ray.getPoint(tm);
_mouseRot.y = 0;
}
鼠标移动事件
if(_rightButtonDown)
{
float2 curPos(absx,absy);
float2 offset = curPos - _mousePos;
_mousePos = curPos;
_camera.rotateViewYByCenter(offset.x * 0.5f,_mouseRot);
_camera.rotateViewXByCenter(offset.y * 0.5f,_mouseRot);
//_camera.rotateViewX(offset.y * 0.5f);
_camera.update();
}
virtual void rotateViewYByCenter(real angle,real3 pos)
{
real len(0);
real len1(0);
matrix4r mat(1);
mat.rotate(angle, real3(0, 1, 0));
real3 vDir = pos - _eye;
len1 = CELL::length(vDir);
vDir = CELL::normalize(vDir);
vDir = vDir * mat;
_eye = pos - vDir * len1;
_dir = _dir * mat;
_up = _up * mat;
_right = CELL::normalize(cross(_dir, _up));
len = CELL::length(_eye - _target);
_target = _eye + _dir * len;
_matView= CELL::lookAt<real>(_eye, _target, _up);
}
virtual void rotateViewXByCenter(real angle,real3 pos)
{
//! 计算眼睛到鼠标点的方向
real3 vDir = pos - _eye;
/// 得到摄像机和旋转点之间的距离
real len1 = length(vDir);
/// 得到摄像机和旋转点之间方向
vDir = normalize(vDir);
real len = 0;
/// 产生旋转矩阵
matrix4r mat(1);
mat.rotate(angle, _right);
vDir = vDir * mat;
/// 计算眼睛的位置
_eye = pos - vDir * len1;
///
_dir = _dir * mat;
_up = _up * mat;
_right = CELL::normalize(CELL::cross(_dir, _up));
len = CELL::length(_eye - _target);
/// 根据眼睛的位置计算观察点的位置
_target = _eye + _dir * len;
_matView = CELL::lookAt<real>(_eye, _target, _up);
}
滚轮滚动
要实现根据根据鼠标位置对视图进行放大缩小,首先需要获取到放置的鼠标与场景的交点。
if (absz)
{
real persent = absz > 0 ? 1.1f : 0.9f;
Ray ray = _camera.createRayFromScreen(absx,absy);
float3 pos = ray.getOrigin();
float tm = abs((pos.y - 0) / ray.getDirection().y);
float3 center = ray.getPoint(tm);
center.y = 0;
_camera.scaleCameraByPos(center,persent);
}
/**
* 指定点推进摄像机
*/
virtual void scaleCameraByPos(const real3& pos,real persent)
{
real3 dir = CELL::normalize(pos - _eye);
real dis = CELL::length(pos - _eye) * persent;
real disCam = CELL::length(_target - _eye) * persent;
real3 dirCam = CELL::normalize(_target - _eye);
_eye = pos - dir * dis;
_target = _eye + dirCam * disCam;
update();
}
首先需要注意的是摄像机看向的方向是一直没变,否则会出现视角一直在转动偏移的情况,先通过摄像机的位置和求出的交点位置计算出方向和距离后求出摄像机的位置,在通过摄像机的位置和方向求出视点位置,在重新计算视图矩阵即可