游戏的在进行一次渲染的时候,通常会提交大量的渲染对象给gpu。在这些需要渲染的对象中,并不是所有对象都会出现镜头中,即有一部分对象是不可见的。
通常有两种方式来完成不可见对象的剔除工作:
(1)直接交给图形库帮我剔除,即性能消耗在gpu端;
(2)在提交图元给gpu前,在游戏逻辑中进行剔除,即性能消耗在cpu端;
是将剔除操作放在cpu还是gpu来处理,没有一个具体的标准,个人认为,要根据实际情况,如果逻辑方面可以快速进行剔除,可以交由cpu来处理,但是gpu在这方面经过了优化,具有更快的处理能力,所以如果cpu端剔除操作过于复杂,可以交由gpu来处理。总之游戏开发中,cpu和gpu的性能都非常重要,所以应该在性能消耗方面尽量平衡,不应该让某一个端承担过多的任务。
opengl是剔除不可见的对象的过程:
一个最简单的模型,坐标点(x,y,z)-> 模型矩阵(modelMatrix)变换 -> 视图矩阵(viewMatrix)变换 -> 投影矩阵(projectMatrix)变换 -> 透视除法(除以齐次坐标中的w)-> 转换为齐次标准裁剪空间坐标 -> 在标准裁剪空间中进行裁剪,剔除不可见的图元
(1)首先在模型空间中定义坐标位置,模型坐标原点通常是(0,0,0),即世界坐标中心点,此时我们可以认为模型坐标系和世界坐标系是重合的,例如(2,2,2)表示相对模型坐标系原点的偏移位置,x正向偏移2,y正向偏移2,z正向偏移2,一个模型中的所有坐标点的变换都是相对于模型坐标系的原点进行设定的;
(2)然后通过视图矩阵将模型坐标系转换为世界坐标系,此时整个模型就显示在世界坐标系中了,例如视图矩阵是将某个模型向x正向偏移100,其实就是将整个模型的所有坐标点向x正向偏移100;
(3)投影矩阵是将世界坐标系中的所有3d坐标投影到2d平面上,游戏中主要采用透视投影,除此之外还有正交投影和斜视投影,正交投影形成的是一个平头截体(frustum),类似一个金字塔被削掉一部分顶部;
(4)透视投影之后形成的齐次坐标(x,y,z,w),为了形成近大远小的透视效果,需要将x/w,y/w,z/w,转换为标准裁剪空间的坐标;
(5)裁剪空间坐标系在不同的图形库中有所不同,opengl的裁剪空间坐标系是x:-1到+1,y:-1到+1,z:0到1的立方体。使用标准裁剪空间的目的:一是标准裁剪空间的剔除效率比在平头截体中剔除更快,相当于在2d矩形中进行剔除;二是标准裁剪空间可以独立于硬件设备。理论上,z坐标的保留在裁剪的时候是多余的,主要是为了后面的深度检测做准备;
备注:
(1)模型矩阵,视图矩阵,投影矩阵,按照规定的先后顺序可以依次结合,modelmatrix * viewmatrix * projectmatrix。由于矩阵乘法适用于结合律不适用于交换律,所以通常结合的方式是modelview矩阵或者viewproject矩阵,没有modelproject矩阵。计算方式有两种:左乘列向量(projectmatrix * viewmatrix * modelmatrix * vector),右乘行向量(vector * modelmatrix * viewmatrix * projectmatrix);
(2)裁剪过程不是简单的剔除不可见的坐标点,在剔除部分坐标点后,会导致一些图元被分割,因此opengl为帮助我们计算剔除后图元和裁剪空间的交点,交点会作为被分割的图元的新坐标点,用于后续的显示;
在游戏逻辑中检测不可见对象,在提交给gpu之前久提前进行了剔除(cocos2d为例):
Vec2 Camera::projectGL(const Vec3& src) const
{
Vec2 screenPos;
auto viewport = Director::getInstance()->getWinSize(); // 获取游戏的设计分辨率
Vec4 clipPos;
getViewProjectionMatrix().transformVector(Vec4(src.x, src.y, src.z, 1.0f), &clipPos); // 将坐标点通过透视投影进行变换,其实在cocos2d中,透视矩阵和一个相机定义的视图矩阵是结合在一起的
CCASSERT(clipPos.w != 0.0f, "clipPos.w can't be 0.0f!");
float ndcX = clipPos.x / clipPos.w; // 使用透视除法,转换成齐次标准裁剪空间坐标
float ndcY = clipPos.y / clipPos.w;
screenPos.x = (ndcX + 1.0f) * 0.5f * viewport.width; // 由于裁剪空间x坐标是从-1到1,要将裁剪空间的坐标映射到屏幕坐标,映射算法是(x - (-1)) / (1 - (-1)) * width
screenPos.y = (ndcY + 1.0f) * 0.5f * viewport.height;
return screenPos;
}
bool Renderer::checkVisibility(const Mat4 &transform, const Size &size)
{
auto scene = Director::getInstance()->getRunningScene();
//If draw to Rendertexture, return true directly.
// only cull the default camera. The culling algorithm is valid for default camera.
if (!scene || (scene && scene->_defaultCamera != Camera::getVisitingCamera()))
return true;
auto director = Director::getInstance();
Rect visiableRect(director->getVisibleOrigin(), director->getVisibleSize());
// transform center point to screen space
float hSizeX = size.width/2; // 用矩形中心点作为检测点
float hSizeY = size.height/2;
Vec3 v3p(hSizeX, hSizeY, 0);
transform.transformPoint(&v3p); // 使用模型视图矩阵去变换坐标,将其转换到世界坐标系中,cocos2d是将图元渲染在z为0的平面上
Vec2 v2p = Camera::getVisitingCamera()->projectGL(v3p); // 使用投影矩阵将世界坐标转换到屏幕坐标
// 计算右上和右下两个坐标点在模型坐标系中旋转后的坐标,用来计算矩形旋转后的最大边界值
// convert content size to world coordinates
float wshw = std::max(fabsf(hSizeX * transform.m[0] + hSizeY * transform.m[4]), fabsf(hSizeX * transform.m[0] - hSizeY * transform.m[4]));
float wshh = std::max(fabsf(hSizeX * transform.m[1] + hSizeY * transform.m[5]), fabsf(hSizeX * transform.m[1] - hSizeY * transform.m[5]));
// 增加可见范围的尺寸,检测可见性
// enlarge visible rect half size in screen coord
visiableRect.origin.x -= wshw;
visiableRect.origin.y -= wshh;
visiableRect.size.width += wshw * 2;
visiableRect.size.height += wshh * 2;
bool ret = visiableRect.containsPoint(v2p);
return ret;
}