这节教程有关于如何运用FrustumCulling(视截体裁剪)提高3D图形渲染效率的,程序的结构如下:
看这节教程前请先看懂以下教程:D3D11字体的实现教程:D3D11教程八之FontEngine(字体实现)
D3D11漫反射光的实现:D3D11教程五之DiffuseLight(漫射光)
下面我分别几小节慢慢阐述。
3D游戏引擎之父约翰卡马克曾说:对游戏而言,效率就是生命。
一,求点到到一个平面的距离。
那么什么是视截体裁剪呢?视截体裁剪就是在3D渲染流水线开始之前就不让完全在视截体外的3D模型数据进入3D渲染流水线,此过程为CPU进行的。
下面我说说为什么要进行视截体裁剪,在说原因前,我再次放出D3D11的3D渲染流水线图:
我们知道,在3D渲染流水线中,在视截体之外的物体不会在世界变换,相机变换和透视投影前(上图红色圈部分)马上被裁剪,而是在齐次裁剪空间被GPU(显卡)进行裁剪算法,把那些不在视截体的部分裁剪掉。这时候问题来了,假设我们的3D世界中有1000个3D球体,一个球体有大概6000个三角面,并且假设在我们的视截体内仅仅有50个球体,那么由于显卡裁剪算法是齐次裁剪空间进行的,那么视截体外的950个球体,得进行世界变换,相机变换,透视投影变换三大变换后才能在齐次裁剪空间被显卡裁剪掉,这里百分之九十五的三角面进行的变换是毫无意义的,可想而知,浪费了显卡极大量的性能。
所以我们得考虑在世界空间就进行手动裁剪,也就是用CPU进行的裁剪算法。那么怎么将球体在世界空间进行FrustumCulling(视截体)呢?先来看看视截体,视截体由6个面组成,即左面(Left),右面(Right),顶面(Top),底面(Bottom),远面(Far),近面(near),下面放出XZ面的截图,YZ面大家自行想象:
想知道一个球体是否在一个视截体之外很简单,先来看看一组图:
上面图中为直观变为2D截面来观看,图中plane为视截体6个面的任意一个面,c点为球体球心,r为球体半径,看为点c到plane的距离,n为plane的单位法向量,以及一个变量d, Po为plane上一个点,则满足Po•n+d=0,由公式1的推导可以知道k=c•n+d,这里我们将球体完全位于视截体之内(图a)或者球体部分与视截体相交(图c)这两种情况都视为相交,这样的球体不能剔除的,而球体完全位于视截体之外则会被剔除(图b),即当k<-r,即c•n+d<-r时被剔除。
最后得注意的是我们球体的球心坐标和视截体的6个平面的表示坐标(XMVECTOR)必须是在同一个空间上才行,这节教程我们是放在世界空间进行计算的。
可以由相机变换矩阵(ViewMatrix)和透视投影矩阵(PerspectiveMatrix)以及视截体的远截面ScrrenFar值 三个参量来 求出世界空间的视截体的6个面的向量(XMVECTOR)表示,这里放出源代码:
求世界空间的视截体的6个面的算法:(目前我还不是很清楚这怎么算的,等我以后想清楚在回来推导原理)
//根据屏幕的深度,投影矩阵和相机矩阵求出世界空间的相应的视截体的6个平面
void FrustumClass::BuildFrustum(float ScreenDepth, CXMMATRIX ProjMatrix, CXMMATRIX ViewMatrix)
{
float zMinimum, r;
XMMATRIX matrix;
XMMATRIX mProjMatrix = ProjMatrix;
XMMATRIX mViewMatrix = ViewMatrix;
//计算视截体近裁剪面的距离
zMinimum = -mProjMatrix._43 / mProjMatrix._33;
r = ScreenDepth / (ScreenDepth - zMinimum);
mProjMatrix._33 = r;
mProjMatrix._43 = -r*zMinimum;
//从相机矩阵和投影矩阵计算视截体矩阵
matrix = XMMatrixMultiply(mViewMatrix,mProjMatrix);
//计算视截体的近裁剪面
XMFLOAT4 nearPlane;
nearPlane.x = matrix._14 + matrix._13;
nearPlane.y = matrix._24 + matrix._23;
nearPlane.z = matrix._34 + matrix._33;
nearPlane.w = matrix._44 + matrix._43;
mPlane[0] = XMLoadFloat4(&nearPlane);
mPlane[0] = XMPlaneNormalize(mPlane[0]);
//计算视截体的远裁剪面
XMFLOAT4 FarPlane;
FarPlane.x = matrix._14 - matrix._13;
FarPlane.y = matrix._24 - matrix._23;
FarPlane.z = matrix._34 - matrix._33;
FarPlane.w = matrix._44 - matrix._43;
mPlane[1] = XMLoadFloat4(&FarPlane);
mPlane[1] = XMPlaneNormalize(mPlane[1]);
//计算视截体的左裁剪面(XZ面)
XMFLOAT4 LeftPlane;
LeftPlane.x = matrix._14 + matrix._11;
LeftPlane.y = matrix._24 + matrix._21;
LeftPlane.z = matrix._34 + matrix._31;
LeftPlane.w = matrix._44 + matrix._41;
mPlane[2] = XMLoadFloat4(&LeftPlane);
mPlane[2] = XMPlaneNormalize(mPlane[2]);
//计算视截体的右裁剪面(XZ面)
XMFLOAT4 RightPlane;
RightPlane.x = matrix._14 - matrix._11;
RightPlane.y = matrix._24 - matrix._21;
RightPlane.z = matrix._34 - matrix._31;
RightPlane.w = matrix._44 - matrix._41;
mPlane[3] = XMLoadFloat4(&RightPlane);
mPlane[3] = XMPlaneNormalize(mPlane[3]);
//计算视截体的顶裁剪面(YZ面)
XMFLOAT4 TopPlane;
TopPlane.x = matrix._14 - matrix._12;
TopPlane.y = matrix._24 - matrix._22;
TopPlane.z = matrix._34 - matrix._32;
TopPlane.w = matrix._44 - matrix._42;
mPlane[4] = XMLoadFloat4(&TopPlane);
mPlane[4] = XMPlaneNormalize(mPlane[4]);
//计算视截体的底裁剪面(YZ面)
XMFLOAT4 BottomPlane;
BottomPlane.x = matrix._14 + matrix._12;
BottomPlane.y = matrix._24 + matrix._22;
BottomPlane.z = matrix._34 + matrix._32;
BottomPlane.w = matrix._44 + matrix._42;
mPlane[5] = XMLoadFloat4(&BottomPlane);
mPlane[5] = XMPlaneNormalize(mPlane[5]);
}
视截体剔除球体的算法
//判断一个球体是否在视截体内,用的是包围球的办法
//由于构建视截体求出的6个面是单位方向向量,因此球心与平面的点积为球心到平面的距离
//假设球心c到视截体6个平面中的任意一平面的距离为k,如果-r>k,则球体位于对应平面的反向之外,即球体完全位于视截体之外,其它情况球体与视截体相交(部分相交或者完全位于视截体)
bool FrustumClass::CheckSphere(float xCenter, float yCenter, float zCenter, float radius)
{
XMVECTOR Point = XMVectorSet(xCenter, yCenter, zCenter, 1.0f);
XMFLOAT4 DotEnd;
for (int i = 0; i < 6; ++i)
{
XMStoreFloat4(&DotEnd, XMPlaneDotCoord(mPlane[i], Point));
if (-radius > DotEnd.x)
{
return false;
}
}
return true;
}
我们这节教程有个类ModelListClass用于生成多个球体的世界空间位置和随机的颜色的
ModelListClass.h
#pragma once
#ifndef _MODEL_LIST_CLASS_H
#define _MODEL_LIST_CLASS_H
#include<Windows.h>
#include<xnamath.h>
#include<time.h>
class ModelListClass
{
private:
struct ModelInfoType
{
XMFLOAT4 color;
float postionX, postionY, postionZ;
};
private:
int mModelCount;
ModelInfoType* mModelList;
public:
ModelListClass();
~ModelListClass();
ModelListClass(const ModelListClass& other);
public:
bool Initilize(int);
void Shutdown();
//Get函数
int GetModelCount();
void GetData(int index, float& positionX, float& positionY, float& positionZ);
XMVECTOR GetModelColor(int index);
};
#endif // !_MODEL_LIST_CLASS_H
ModelListClass.CPP
#include"ModelListClass.h"
ModelListClass::ModelListClass()
{
mModelList = NULL;
}
ModelListClass::~ModelListClass()
{
}
ModelListClass::ModelListClass(const ModelListClass& other)
{
}
//随机生成numModel个球模型
bool ModelListClass::Initilize(int numModel)
{
int i;
float red, green, blue;
//首先存储模型数量
mModelCount = numModel;
//创建模型数据的数组
mModelList = new ModelInfoType[mModelCount];
if (!mModelList)
{
return false;
}
//用现有时间初始化随机种子
srand((unsigned int)time(NULL));
//给每个球模型赋予随机性的数据
for (int i = 0; i < mModelCount; ++i)
{
//生成随机颜色[0,1]范围
red = (float)rand() / RAND_MAX;
green = (float)rand() / RAND_MAX;
blue = (float)rand() / RAND_MAX;
mModelList[i].color = XMFLOAT4(red, green, blue, 1.0f);
//在相机前面为模型生成随机位置
mModelList[i].postionX = (((float)rand() - (float)rand()) / RAND_MAX)*20.0f; //[-20.0, 20.0];
mModelList[i].postionY = (((float)rand() - (float)rand()) / RAND_MAX)*20.0f;
mModelList[i].postionZ = ((((float)rand() - (float)rand()) / RAND_MAX)*20.0f) + 5.0f; //[-15.0f,25.0f]
}
return true;
}
void ModelListClass::Shutdown()
{
//释放模型数组的数据
if (mModelList)
{
delete []mModelList;
mModelList = NULL;
}
}
int ModelListClass::GetModelCount()
{
return mModelCount;
}
void ModelListClass::GetData(int index, float& positionX, float& positionY, float& positionZ)
{
positionX = mModelList[index].postionX;
positionY= mModelList[index].postionY;
positionZ= mModelList[index].postionZ;
}
XMVECTOR ModelListClass::GetModelColor(int index)
{
return XMLoadFloat4(&mModelList[index].color);
}
最后在我的源代码说明下程序的操作:A键相机视角往左旋转,D键视角往右旋,按着Q键不松开则关闭FrustumCull,反之开启FrustumCull。
我们这节测试的球体有800个,一个球体大概6000个面,每个球体的位置是随机生成的。
一,开启FrustumCull,此时进行渲染的球体(也就是真正在视截体内的)有362个,剩余的438个球体在世界空间就被剔除出视截体了,图形渲染稳定时的帧数为305,CPU消耗:30%,如下面图所示:
二,关闭FrustumCull,此时整个空间有800个球体,不管在不在视截体内的球体都进行了世界变换,相机变换,透视投影变换,而且被显卡进行裁剪,图形渲染稳定时的帧数为165,CPU消耗:19%,如下面图所示:
从上面的对比可知 FrustumCull技术让我们以低昂的CPU花费降低了GPU的性能消耗,降低了GPU的负担,提升了游戏帧数。
我的源代码链接如下: