Directx11教程三十七之FrustumCulling(视截体裁剪)上半节教程

这节教程有关于如何运用FrustumCulling(视截体裁剪)提高3D图形渲染效率的,程序的结构如下:





看这节教程前请先看懂以下教程:D3D11字体的实现教程:D3D11教程八之FontEngine(字体实现)

                                                            D3D11漫反射光的实现:D3D11教程五之DiffuseLight(漫射光)


下面我分别几小节慢慢阐述。


3D游戏引擎之父约翰卡马克曾说:对游戏而言,效率就是生命。

一,求点到到一个平面的距离。

在这篇博客我们已经推导出 3D平面的通用公式,假设一个平面的单位法向量为n(nx,ny,nz),这里请注意是单位法方向,而非仅仅是法向量,在假设有一个距离d,平面上的任意一点为Po(x,y,z), 则平面公式可以用 n•Po+d=0 表示, 在xnamath.h数学库中平面公式用向量XMVECTOR(nx,ny,nz,d)表示.
假设空间有一个点P(Px,Py,Pz),那么如何求点P到平面  n•Po+d=0的距离呢?先来看看下图:



                                                                      图(1)

看上面图,这就是我们问题描述的模型了,有人会问了,你怎么知道单位法向量刚好在Po点之上?其实单位法向量仅仅是个方向向量,方向向量是没有位置的,也就是不能进行数学意义上的平移的,你可以随意的把单位法向量放在平面上任一位置,这里移动到刚好在Po之上,仅仅是为了直观的认识罢了。

求出向量PoP=P-Po, 而PoP•n=∣PoP∣*∣n∣*  cosΘ=∣PoP∣*cosΘ ,  所以P点到屏幕的距离h=PoP•n,
下面化简PoP•n,  h=PoP•n=(P-Po)•n=P•n-Po•n=P•n+d-(Po•n+d)

上面说过Po在屏幕上,因此Po•n+d=0,最终化简得到h=P•n+d-(Po•n+d)=P•n+d-0=P•n+d;

                                                                                    h=P•n+d   (公式1)
                                            
这里请注意的一点是法向量得是单位的法向量才成立.

下面延展开来:

由公式1和上面的推导可以知道三点:
规定法向量n的方向为平面正方向
(1)当P•n+d>0时,P在平面的正方向,即正距离。
(2)当P•n+d=0时,P在平面上,即零距离。
(3) P•n+d<0时,P在平面的负方向,即负距离。
如下面图所示:






二,包围球,AABB体,包围正方体,包围长方体。

在我们的3D渲染程序中,为了方便某些东西的计算,经常会给某些3D物体套上一个马甲--------包围体,比较有名的包围体有BoundingSphere(包围球体),
axis-aligned bounding box(简称AABB体),以及包围正方体和包围长方体等等,实际上包围正方体和包围长方体属于AABB体。如下面图所示:




看上面图,在3D渲染中有一架飞机,左边为包围这架飞机的AABB体,右边为包围这架飞机的包围球。包围球和AABB体经常用于辅助实现FrustumCull技术(视截体裁剪)和Pick技术(屏幕拾取3D物体).

由于一些原因,我在这不讲述如何从3D模型的顶点坐标数据来求出相应AABB体和包围球体了,以后回来补充。


三,视截体裁剪。

我们的视截体如图所示:



那么什么是视截体裁剪呢?视截体裁剪就是在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的负担,提升了游戏帧数。


我的源代码链接如下:

http://download.csdn.net/detail/qq_29523119/9681864

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值