首页 > 代码库 > ZFXEngine开发笔记之Omni Light

ZFXEngine开发笔记之Omni Light

作者:i_dovelemon

日期:2014 / 9 / 5

来源:CSDN博客

主题:Per-pixel lighting, Omni light, early-z, Multi-pass, Assembly Shader


引言

             在第一人称射击游戏中,经常需要点光源来完成光照计算。所以,这篇文章中将会讲述如何创建这个点光源,并且如何设计让引擎可以支持多光源渲染。


Omni light介绍

              所谓的Omni light,就是指点光源。不过在这里,我们进行点光源的光照计算的时候,没有用到顶点的法线来进行光照计算。Omni light具有两个属性,分别是在世界坐标空间中的位置position,以及在光源的照射半径radious。有了这两个属性,我们就可以对顶点进行光照计算了。

             由于在下面,我将使用Assembly Shader的方式来进行Shader的编写,并且,我们需要根据Omni light的两个属性,来设置顶点的衰变参数。衰变参数指的是,在距离光源越近的时候,顶点被照射的越亮,距离越远的时候越暗,乃至于超出了Omni light设定的光照半径之后,就不在进行光照。

             所以,我们需要一个矩阵,一个能够将顶点与Omni light的衰变参数计算出来的矩阵。

             首先,为了计算的方便,我先假设,物体模型的世界变换矩阵是单位矩阵。那么就是说,Omni light的位置坐标和模型上的顶点坐标可以认为在一个空间中,不需要进行坐标变换了。(注:如果模型的世界变换坐标并不是单位矩阵的话,就需要进行变换)

             然后,我将Omni light的光源位置平移到世界坐标的原点处,用同样的变换矩阵对顶点进行变换的话,那么顶点的坐标就表示了在各个轴上距离光源的距离。

             在有了距离之后,我们需要进行一些变换,将衰变参数控制在0-1之间,在0-1之间能够方便的对光源进行颜色计算,并且在Pixel Shader中,所有的输入数据都要在0-1之间,就算不是这个区间的值,也会被强制的缩减到0-1之间。

             下面是进行此种变换的代码:

<span style="font-family:Microsoft YaHei;">ZFXMatrix CalcOmniAttenuMatrix(ZFXVector pos, float radious)
{
	ZFXMatrix mT ;
	mT.identity();
	mT.translate(-pos.x, -pos.y, -pos.z);

	ZFXMatrix mS ;
	mS.identity();
	float invRadious = 0.5f / radious ;
	mS._11 = mS._22 = mS._33 = invRadious ;

	ZFXMatrix mB ;
	mB.identity();
	mB.translate(0.5f, 0.5f, 0.5f);

	mT = mT * mS ;
	mT = mT * mB ;

	ZFXMatrix mResult ;
	mResult.transposeOf(mT);

	return mResult ;
}// end for CalcOmniAttenuMatrix</span>
              上面的代码,先进行平移操作,也就是mT的操作,这个操作会将顶点平移,然后进行缩放操作mS。这里对缩放操作的缩放因子为0.5 / radious。读者可以想象一下,radious是光源的光照半径,缩放时,对顶点的三个分量,比如x/raidous * 0.5,也就是说如果顶点在X方向的半径范围内,那么这个缩放后的值就会变成了-0.5-0.5之间的值了。在加上最后的一个平移0.5的操作,那么就变成了0-1.0的范围了。这样,刚好满足Pixel Shader的输入条件。

             注意,在最后,我么将计算出来的矩阵,进行了转置操作,这是因为在Assembly Shader中,进行向量和矩阵的乘法运算时,Assembly Shader的计算方式,是按照向量和矩阵的行相乘,而不是数学上的和列相乘。这样做的原因是进行行相乘的寻址更加简单。如果按列相乘的话,那么还要跨越寄存器去取值,就会给计算带来额外的开销。读者可能会想,c20不就是一个向量的单位吗?怎么能容下一个矩阵了?这是GPU的处理机制,如果c20容纳不下,那么就会使用c21,c22,c23来继续容纳。知道容纳下为止。也就是说,c20,c21,c22,c23这四个寄存器保存了一个矩阵的数据。

            下面就是这个光源的Vertex Shader和Pixel Shader的代码:

<span style="font-family:Microsoft YaHei;">vs.1.1
dcl_position0 v0
dcl_normal0   v3
dcl_texcoord0 v6
m4x4 oPos, v0, c0
mov oT0, v6
m4x4 oT1, v0, c20
</span>

<span style="font-family:Microsoft YaHei;">ps.1.1
tex t0
texcoord t1
dp3_sat r0, t1_bx2, t1_bx2
mul r0, c1, 1-r0
mul r0, r0, t0</span>

            在前面一篇文章中已经讲述了如何使用GPU中的各种寄存器。这里不再赘述。

            上面的Vertex Shader,除了对顶点进行基本的变换操作之外,就是使用上面计算出来的矩阵对顶点进行变换,并且将结果保存在oT1寄存器中,供Pixel Shader使用。

            在Pixel Shader中,使用texcoord指令将t1中的数据取出,而不是tex指令。tex指令是使用t0中的坐标来对纹理进行采样,并且把采样后的纹素值保存在t0中。在有了t1这个值,也就是各个顶点三个轴距离Omni light的距离之后,使用了如下的指令:

            dp3_sat r0, t1_bx2, t1_bx2

           这条指令中的t1_bx2是将t1中的值减去0.5,然后再乘以2。在前面说过,Pixel Shader中的输入寄存器中的值只能是0-1。所以,经过这样的变换之后,就变成了-1到1的范围,然后使用dp3指令,计算出这个坐标距离原点(也就是Omni light)的距离的平方,dp3_sat会增加一个操作,那就是将dp3的计算结果clamp到0-1这个范围来。

           这样,我们就有了一个平方衰变参数。进行衰变时,一般很少使用线性的衰变方法,加上平方之后,衰变的效果更加的好。有了衰变参数之后,我们用1-r0,那么就能够得到光源的光照强度了。衰变参数越低,光照强度就越高,不是吗?c1中存放了光源的颜色属性。使用衰变参数来获取颜色,并且与Diffuse Texture进行混合,就会得到最后的像素值。保存在r0中作为输出像素。


Early-Z介绍

          Early-Z技术,是为了能够让Pixel Shader执行比较复杂的运算,而不降低效率而创建出来的。Early-Z检测会在Pixel Shader之前进行,它会先将无法通过Z测试的数据剔除掉,从而节省进行Pixel Shader计算的开销。传统的Z-Test是在Pixel Shader之后,进行的。如果只有这个的话,那么就没有办法在Pixel Shader之前,剔除掉数据,Pixel Shader就需要对很多在接下来Z-Test中被剔除的数据进行计算,实在是浪费资源。所以Early-Z技术能够提高Pixel Shader的效率。但是Ealry-Z是硬件的隐式特许。也就是说,DirectX API没有什么函数能够指定它是运行还是不运行。默认没有开启Alpha blend的情况下,它就是开启的。这是因为Ealry-Z技术和Alpha Blend技术有冲突。


Multiply Light and Multiply-Pass

         实现多光源渲染的方法有很多。这里使用的是一种Multiply-pass渲染技术。也就是说,每一次,对一种光源渲染一次场景,然后将渲染的结果与前面一次渲染的结果进行Alpha Blend,从而实现多光源渲染的效果。

          在引擎中设置了一个m_bAdditionBlend的参数,这个参数就是控制是否开启Multi-pass渲染。如果你需要对此渲染的话,那么就需要将这个变量设置为true,引擎会在最终渲染的时候,根据这个变量来设置Alpha blend,从而来进行Alpha Blend。

          下面是进行设置的函数:

<span style="font-family:Microsoft YaHei;">void ZFXD3D::useAdditiveBlending(bool b)
{
	if(m_bAdditive == b)
		return ;

	//clear all vertex cache
	m_pVertexMan->forcedFlushAll();
	m_pVertexMan->invalidateStates();

	m_bAdditive = b ;
	if(!m_bAdditive)
	{
		m_pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
		m_pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
		m_pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
	}
}// end for useAdditiveBlendingbool ZFXD3D::useAdditiveBlending(){    return m_bAdditive ;}// end for useAdditiveBlending</span>

           一旦取消的时候,就关闭Alpha Blend。启动之后,不做任何事,因为在最终的渲染代码中有如下一段代码:

<span style="font-family:Microsoft YaHei;">//should we use additive blending
if(m_pZFXD3D->useAdditiveBlending())
{
	m_pDevice->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
	m_pDevice->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ONE);
	m_pDevice->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);
}</span>
          通过这样的方法,就能够实现多次的渲染,从而实现多光源。


程序截图:


           今天的笔记到此结束!

ZFXEngine开发笔记之Omni Light