这篇文章记录的是游戏引擎开发当中,最普遍的镜头操作。玩过《王者荣耀》《绝地求生》《阴阳师》等热门手游的同学,应该就知道这个镜头操作是多么的普遍。接下来围绕这个镜头操作show一波代码。相关工程地址:https://github.com/MrZhaozhirong/NativeCppApp
#include "CELLMath.hpp"
using namespace CELL;
class Camera3D
{
public:
real3 _eye; //视点
real3 _target;//观察点
real3 _dir; //观察向量:视点->观察点
real3 _up; //观察向量竖直向量
real3 _right; //观察向量右向向量
matrix4r _matView; //视图矩阵
matrix4r _matProj; //投影矩阵
matrix4r _matWorld; //世界坐标
real2 _ViewPortSize; //视口大小
Camera3D( const real3& target = real3(0,0,0),
const real3& eye = real3(0,10,10),
const real3& right = real3(1,0,0) )
{
_target = target;
_eye = eye;
_right = right;
_up = normalize(cross(_right, _dir));
_dir = normalize(_target - _eye);
// 初始化单位矩阵
_matView = CELL::matrix4r(1);
_matProj = CELL::matrix4r(1);
_matWorld = CELL::matrix4r(1);
}
~Camera3D() {}
};
首先我们来看看Camera3D的组成,其中real2 real3 matrix4r 都是之前介绍过的CELLMath.hpp的自定义对象,real代表的是小数精度值,尾数2/3/4代表的是元素的个数,real2就是(x,y),real3代表(x,y,z),real4代表(x,y,z,w)已经够满足我们在OpenGL的使用范围了。matrix4r也很好理解,就是4x4的矩阵空间。 (其中自定义对象还包含了相关功能方法,运算符重载,以及Cpp模板的运用,需要一定的C++知识和线性代数知识才能掌握,有时间的同学强烈建议多次阅读。)
_eye:视野点,标志我们眼睛所在的位置;_target:观察点,指明我们眼睛所要注视的目标位置;
_dir:观察方向,向量_target - 向量_eye = 向量(eye->target),方向是减数指向被减数,这是高中数学的几何知识;
_up和_right就是两个方向向量,用于指明我们眼睛的观察方向,两者互相垂直,然后和_dir观察方向两两垂直;
基于这个垂直的特性,我们可以通过任意两个向量的叉积运算(cross)推算出另外一个向量的方向,然后通过(normalize)归一化处理得出单位长度。
如下示意图所示:黄色点为_eye,黑色点为_target;从黄色指向黑色的金色线,就是_dir;蓝色线就是_up;灰色自然就是_right;由此可见,三者两两构成一个平面并且在这个平面内垂直,构成一个完整的摄像机坐标体系。(但千万别和世界坐标系搞混)
然后就是三个矩阵:_matView视图矩阵,_matProj投影矩阵,_matWorld世界坐标矩阵;
前两个矩阵之前已经介绍过了,有问题请到这里。怎么理解这个_matWorld世界坐标矩阵,其实就是一个模型矩阵,只不过这个模型就是针对整个OpenGL世界坐标,没必要都不要随随便便的缩小放大平移旋转世界(_matWorld)这个全局模型。
virtual void update()
{
_matView = CELL::lookAt(_eye,_target,_up);
}
// 正交投影
void ortho( real left, real right, real bottom, real top, real zNear, real zFar )
{
_matProj = CELL::ortho(left,right,bottom,top,zNear,zFar);
}
// 透视投影
void perspective(real fovy, real aspect, real zNear, real zFar)
{
_matProj = CELL::perspective(fovy,aspect,zNear,zFar);
}
相关方法如上,其他那些get/set就不贴出来了节省篇章。核心实现都在CELLMath.hpp。
0、介绍简单的镜头操作
需求实现:
手指左右滑动屏幕,镜头能围绕着观察点(_target)左右旋转;
手指上下滑动屏幕,镜头也能围绕观察点上下调整视点(_eye);
双指操作,能 拉近/推离 观察点(_target)到视点(_eye)的距离;
在SurfaceView添加OnTouchListener,解析UP/DOWN/MOVE等相关事件,大致代码如下:
private class GLViewTouchListener implements View.OnTouchListener {
private int mode = 0;
private float oldDist;
@Override
public boolean onTouch(View v, MotionEvent event) {
// -------------判断多少个触碰点---------------------------------
switch (event.getAction() & MotionEvent.ACTION_MASK ){
case MotionEvent.ACTION_DOWN:
mode = 1;
break;
case MotionEvent.ACTION_UP:
mode = 0;
break;
case MotionEvent.ACTION_POINTER_UP:
mode -= 1;
break;
case MotionEvent.ACTION_POINTER_DOWN:
oldDist = spacing(event);
mode += 1;
break;
}
if(event.getAction() == MotionEvent.ACTION_DOWN){
if(mode == 1) {
final float x = event.getX();
final float y = event.getY();
nativeEGL.handleTouchDown(x, y);
}
}else if(event.getAction() ==MotionEvent.ACTION_MOVE){
if (mode == 1) {
final float x = event.getX();
final float y = event.getY();
nativeEGL.handleTouchDrag(x,y);
}
if (mode == 2) {
//双指操作
float newDist = spacing(event);
if ( (newDist > oldDist + 20) || (newDist < oldDist - 20) ) {
final float distance = newDist - oldDist;
nativeEGL.handleMultiTouch(distance);
oldDist = newDist;
}
}
}else if(event.getAction() == MotionEvent.ACTION_UP){
if (mode == 1) {
final float x = event.getX();
final float y = event.getY();
nativeEGL.handleTouchUp(x,y);
}
}
return true;
}
}
先分析单指的滑动操作。直接贴上相关代码:
void GL3DRender::handleTouchDown(float x, float y) {
this->mLastX = x;
this->mLastY = y;
}
void GL3DRender::handleTouchDrag(float x, float y) {
float offsetX = this->mLastX - x;
offsetX /= 10;
mCamera3D.rotateViewY(offsetX);
float offsetY = this->mLastY - y;
offsetY /= 50;
mCamera3D.rotateViewX(offsetY);
this->mLastX = x;
this->mLastY = y;
}
void GL3DRender::handleTouchUp(float x, float y) {
this->mLastX = 0;
this->mLastY = 0;
}
handleTouchDrag函数就是处理滑动的逻辑,传入每一时刻滑动的屏幕坐标点位置,用传入坐标点与上一时刻的坐标点进行减法运算得出偏移值。
1、镜头随屏幕左右滑动
屏幕坐标系x是横向,需求是随着滑动而左右旋转,即沿着OpenGL的坐标系的Y轴为轴中心进行旋转,逻辑实现在rotateViewY方法当中,接下来就看看详细的实现。
/**
* 下面的函数的功能是将摄像机的观察方向绕某个方向轴旋转一定的角度
* 改变观察者的位置,目标的位置不变化
*/
virtual void rotateViewY(real angle)
{
real len(0);
matrix4r mat(1);
mat.rotate(angle, real3(0, 1, 0));
// 定义单位矩阵,沿着Y轴旋转angle个角度。
_dir = _dir * mat; //新的观察方向
_up = _up * mat; //新的头顶方向
_right = CELL::normalize(cross(_dir, _up));
//用新的_dir和_up计算出新的_right
//接下来重点算出新的视点
// 先用旧的_eye和_target计算两点间的距离长度,这相当于旧的_dir.length
len = CELL::length(_eye - _target);
// 然后保持观察点不变,利用新的_dir为方向,len为步伐,计算出新的_eye
_eye = _target - _dir * len;
// 更新视图矩阵
_matView = CELL::lookAt(_eye, _target, _up);
}
从注释我们知道整个数学逻辑过程,值得注意的就是计算新的视点:很多同学忘了要找出len这一个步长,这个len才是真正的_eye和_target之间的空间距离长度,_dir = _target - _eye 这是一个方向向量,是从_eye出发指向目的点在_target。两者意义相差还是蛮大的。既然 dir = target - eye,那么旋转后的eye = (保持不变)target - 旋转后的dir*len,最后更新视图矩阵。
效果图:
2、镜头随屏幕上下滑动
同理屏幕坐标y是纵向,需求是随着滑动上下调整,即沿着OpenGL的坐标系的地平线 (也可以理解为X轴但又不是X轴) 为轴中心进行旋转,逻辑实现在rotateViewX方法当中。
virtual void rotateViewX(real angle)
{
real len(0);
matrix4r mat(1);
mat.rotate(angle, _right);
// 重点 为何不是的模板代码?real3(1, 0, 0)
_dir = _dir * mat;
_up = _up * mat;
_right = CELL::normalize(cross(_dir,_up));
len = CELL::length(_eye - _target);
_eye = _target - _dir * len;
_matView = CELL::lookAt(_eye,_target,_up);
}
代码逻辑和上方rotateViewY大致是相同的,唯一不同点就是旋转的轴线,为啥不是模板代码 mat.rotate(angle, real3(1, 0, 0)); 这是因为并不是固定围绕着OpenGL的X轴旋转,是沿着水平方向旋转,这一点请必须理解清楚!
不明白的同学试下一下,如下操作情形:开始摄像机在(0,0,10)位置上,正对着XY的平面,然后向右rotateViewY(90)到达了新的位置(10,0,0),此时你的水平方向还是X轴的方向了吗?显然不是。
那么有同学又可能会反问,为何rotateViewY方法就不是 mat.rotate(angle, _up)? 因为这个左右旋转的逻辑,确实就是沿着Y轴旋转啊!还是一样的举例说明:开始摄像机在(0,0,10)位置上,正对着XY的平面,然后沿着原本的_up向上rotateViewX(90)到达了新的位置(0,10,0)正对着XZ平面,现在的_up是沿着Z轴方向,显然并不是我们想要旋转轴心。
效果图:
3、拉近/推离摄像机和观察点距离
void GL3DRender::handleMultiTouch(float distance) {
LOGD("handleMultiTouch distance:%f", distance);
real present = distance > 0 ? 0.9f : 1.1f;
real3 target = mCamera3D.getTarget();
mCamera3D.scaleCameraByPos(target, present);
}
逻辑接口很简单,通过每一时刻双指间的差值判断其是否在拉伸or靠近。
真正实现是scaleCameraByPos,通过命名可以知这是一个指定定点,按照定点位置缩放相机。这里我们一直注视着_target,所以pos = _target。然后深入分析背后的几何数学原理。
// 指定点推进摄像机
virtual void scaleCameraByPos(const real3& pos,real present)
{
// 计算眼睛到定点的方向
real3 dir = CELL::normalize(pos - _eye);
// 计算眼睛到定点 下一刻 的距离,即拉近/伸远的距离
real dis = CELL::length(pos - _eye) * present;
// 计算眼睛到当前观察点的距离
real disCam = CELL::length(_target - _eye) * present;
// 计算眼睛到当前观测点的方向向量
real3 dirCam = CELL::normalize(_target - _eye);
// 新的眼睛 = pos - 新的dir*新的距离dis
_eye = pos - dir * dis;
// 再利用新的眼睛位置,推算新的target
_target = _eye + dirCam * disCam;
_matView= CELL::lookAt(_eye, _target, _up);
}
这是一个通用的方法,因为pos是任意一点,这样我们就可以仿照其他3D游戏定点自动走路的功能了。
效果图:
相关工程源码: https://github.com/MrZhaozhirong/NativeCppApp 参考GL3DRender.cpp Camera3D.hpp