作者:i_dovelemon
日期:2014 / 9 / 5
来源:CSDN博客
主题:Per-pixel lighting, Omni light, early-z, Multi-pass, Assembly Shader
在第一人称射击游戏中,经常需要点光源来完成光照计算。所以,这篇文章中将会讲述如何创建这个点光源,并且如何设计让引擎可以支持多光源渲染。
所谓的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>
上面的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技术,是为了能够让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-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 useAdditiveBlending bool ZFXD3D::useAdditiveBlending() { return m_bAdditive ; }// end for useAdditiveBlending </span>
<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>通过这样的方法,就能够实现多次的渲染,从而实现多光源。
程序截图:
今天的笔记到此结束!