引言
在计算机图形中,为了使的图形看上去具有立体的效果,我们需要为我们的3D物体,加上一点光照效果。如果你学过绘画的课程,就会知道,一个物体,如何想要画的具有立体的感觉,我们需要对物体进行光照处理。比如说,我们要给物体画上暗部,亮部,高光部分,反光部分,还有中间的光暗过度区部分。画家是通过自己的感觉和艺术修养来对物体进行光照处理,而我们程序员有我们自己的方法。虽然我们没有那么好的美术功底,但是,我们可以通过建立光照模型,对物体进行着色,通过精确的模型对物体进行着色,要比画家绘制的立体物体要更加的真实点,但是会缺少相应的艺术感。如果你想要一个艺术话的光照模型,可以使用catton着色效果来对物体进行着色,很多著名的游戏如《无主之地》系列等就是使用catton着色的艺术效果来制作的。
好了,在接下来的文章中,将会向大家讲述如何搭建一个比较真实的光照模型。
物体为什么会有颜色?
这个缤纷的世界,充满了各种各样的色彩,但是,读者您有没有想过,为什么物体会具有这样或者那样的色彩了?
上帝在创造世界的时候,给予了光,和人类感受光的能力。物体的颜色也是通过光和物体本身的交互,再加上人类对光的感受能力,才形成的。在这里,不会和大家讲述很详细的形成过程,那要涉及大量我自己都没有掌握的知识,所以,这里只是简要的介绍下,颜色是如何形成的。
一个物体,它具有对光进行吸收和反射的能力。这种能力在图形学中,我们使用材质属性来表示。材质属性,定义了它会对光源进行什么样的吸收和反射。不同的材质,具有不同的特性。大家都知道,在计算机中表示颜色一般是使用RGB的方式,用R 表示Red红色, G表示Green 绿色, B表示Blue蓝色。我们同样的,可以使用这样的颜色值来定义一个光源的颜色,从而在通过材质属性,对不同的分量,进行不同的吸收和反射。这样就可以模拟出一个基本的物体和光交互的方法。
但是,读者可能会想到,这只是,物体和光之间进行交互的方式罢了。那么为什么我们能够看到了?
人类的眼睛很奇妙,它的内部有很多很多微小的感应神经。这些神经元对光的强度很敏感,所以,当光照射到人眼中之后,这些传感器就会接受到不同强度的光照射,然后经过一些复杂的生物变化,形成信号,发送到了我们的大脑,大脑根据这些信号来形成一个图像,那就是我们看到的东西。
所以,与其说物体具有颜色,还不如说,是物体给予我们眼睛某种颜色的感觉。通过上面的描述,我们也会发现,材质定义的巧妙之处。材质定义了,物体在光源照射下,将会反射什么样的颜色,如果这样的颜色被我们的眼睛接受到,那么,我们就会觉得这个物体就是它反射的颜色。
所以,通过人眼,材质,和光源属性,我们就能够模拟出一个简单的光照模型出来。
平行光
在这里,先介绍一个最基本的光源,平行光。
一般来说,我们认为太阳,照射到地球表面上一个很小的面积时,太阳光是平行的照射过来的,所以这样的光我们称之为平行光。
漫反射
当平行光,照射到一个表面粗糙的物体上的时候,由于表面很是粗糙,光线会朝着不同的方向进行反射。而一个物体表面的粗糙程度要复杂的多,所以,我们大可以认为,粗糙表面的物体,会将光线反射到任何方向上去。所以,只要视线不被遮挡,我们的眼睛总是能够看到这样的物体。而表面非常光滑的物体,可能会将光线反射到某一个特定范围上的方向,比如玻璃,这也就是为什么经常有粗心的人会撞到玻璃上去,因为他的眼睛没有接受到玻璃反射过来的光源,自然就不能够看到了。不过,实际上,绝对光滑的物体是不存在的,否则,这样利用光线进行反射,说不定,可以做出隐身衣哦>_<
顶点的法向向量
好了,上面闲话已经说了很多了,现在来写理论的东西。我们都知道,在3D图形学中,我们使用顶点来表示多边形,然后利用多边形构建出物体。为了能够进行光照处理,我们需要知道每个顶点的法向向量。如果读者不知道什么是法向向量,可以参考下高中的物理或者几何数学。这里不再赘述。
对于一个平面来说,它的法向向量是唯一的,这个法向向量垂直于这个平面上任何两个不重合点的连线。额,读者可能觉得,这样的向量有两个,那到底是哪一个了???
的确,这样的向量的确有两个,要确定一个平面的法向向量,我们还需要知道,这个平面上的点是通过什么样的绕序(winding order)来定义的。在DirectX中,我们是使用顺时针的绕序方式来定义一个多边形的。(这是基本的DirectX知识,如果读者对这些知识,不甚了解的话,可以在优快云中搜索浅墨,在他的博文中,会详细的讲述DirectX的方方面面,是个学DirectX的好地方)。所以,对于下面的图形:
我们假设:U = P1 - P0 , V = P2 - P1, 那么 这个平面的法向向量即为N = Cross(U, V)这里的Cross是指对向量进行叉积运算,如果读者不是很明白这里,可以阅读我博客中关于向量的介绍文章DirectX 9.0 向量。在向量一章中,我们讲述了Cross的作用就是产生一个垂直于表面的向量(读者,注意,本系列文章使用的是DirectX,所以,一直采用的都是左手坐标系)。通过这样的方式,我们就能够计算出一个表面的法向向量了。
上面讨论的问题,过于的简单,我们需要在此基础上进行扩展。往往,在3D空间中的一个物体都是由很多的多边形平面组合而成的,而这样的方式,就会导致可能会出现有多个平面共享某一个顶点的情况,对于这样的顶点,我们就不能简单的使用平面的法线向量来表示顶点的法向向量了。这里,需要经过一些平均方法来进行计算。我们考虑如下图中的情况:
对于这样的一个多边形网格图,我们要计算中间那个顶点的法向向量的话,就无法简单的使用一个表面的法向向量来表示了,因为这个顶点被5个不同的表面所共享,我们需要通过如下的公式来表示这个顶点的法向向量:
好了,关于顶点的向量,我们就讲到这里。接下来的内容将会更加的重要。
对法向向量进行变换
我们知道,在3D流水线中,充满了各种各样的变换。当我们对一个顶点进行变换的时候,它的向量也应该随着一起改变才可以。但是,怎么样对法向向量进行变化,才能保证变换后的向量依然与顶点保持垂直关系了???
我们知道,对于任意的一个向量u,都可以使用空间中的两个点相减来得到,即u = v1 - v2 , 所以如果用矩阵A对向量u进行变换,即uA = (v1 - v2)A = v1A - v2A。
我们知道原来的法向向量n与u之间的关系可以使用Dot(u,n) = 0来表示
而Dot(u,n) = 0 又可以表示成 u Transpose(n),将n进行转置,我们得到一个3*1的矩阵,而u原本是一个1*3矩阵,所以他们相乘的结果与Dot(u,n)的结果一致。
我们再在左边乘上一个单位矩阵I, 即 uI Transpose(n) = 0,其结果将保持不变
而I又可以拆开变成一个可逆的矩阵与它的逆矩阵的乘积,所以,将变换矩阵考虑进去,得到I = A * Inverse(A)
带入公式中得到:
u (A * Inverse(A)) Transpose(n) = 0
可以继续将上面的公式写成如下的格式:
u A (Inverse(A) Transpose(n)) = 0
这样,就可以继续转化为如下的公式:
u A Transpose(n Transpose(Inverse(A))) = 0
而同样的,我们又可以将1*3和3*1矩阵的乘法,转化为点积运算,即:
Dot(u A, n Transpose(Inverse(A))) = 0
通过上面的公式,我们就可以得到,如果对向量u进行A变换,那么需要对原来的法相向量进行B = Transpose(Invese(A)) 变换,他们才会依然保持关系。
好了,说了这么多,意思就是如果在对顶点进行变换的过程中,我们使用了A矩阵进行变换,那么,我们就需要使用A矩阵的逆矩阵的转置矩阵,即Transpose(Inverse(A))来对原来法相向量进行变换,才能够保证,他们之间的关系依然是正交的。
漫射光模型
我们知道,当一束光,直射到眼中的时候,这个时候感觉到的光的强度是最强的,而当光照角度越来越大的时候,这的强度越来越弱,并且当光照角度大于90度之后,就表示照射到后表面上去了,这时光照强度为0。
为了模拟出这样的情况,我们使用中学数学课程中余弦cos来满足这样的情况。我们知道cos函数在0 - 90度时,它的值域是慢慢下降的,到了90度时,它的值刚好就是0,所以刚好能够模拟我们现在漫射光模型。
我们使用一个公式来表示上面描述的情况:
这里,我们使用Dot(L,n),L是光照向量,n为法向向量,并且他们都是单位向量。注意,这里的光照向量,需要特殊介绍下。
光照向量,并不是指从光源指向顶点,而是从顶点指向光源的向量。需要读者自己仔细区分。
好了,上面的公式,模拟了光照强度的衰减过程,但是还是没有模拟出光源颜色和材质之间的关系,所以在上面的公式,添加上关于光源颜色和材质:
上面的东西Clight*Material(diffuse)表示的就是他们的各个分量相乘的结果,如下所示:
Cliight = (0.8, 0.8, 0.8) Material(diffuse) = (0.5, 0.0, 1.0)
Clight * Material(diffuse) = (0.8 * 0.5 , 0.8 * 0.0, 0.8 * 1.0) = (0.4, 0.0, 0.8)
好了,有了上面的理论知识,我们就可以来实际动手实验一下。
实例代码解释
下面是产生Cube立方体的顶点和索引的代码:
void CubeDemo::genCube()
{
//Create the Cube vertex buffer
HR(m_pDevice->CreateVertexBuffer(8*sizeof(VertexPN), D3DUSAGE_WRITEONLY,0,D3DPOOL_MANAGED,
&m_pVertexBuffer,0));
//Lock the index buffer
VertexPN * _pData = NULL ;
HR(m_pVertexBuffer->Lock(0,0,(void**)&_pData,0));
_pData[0]._pos = D3DXVECTOR3(-1,1,-1); _pData[0]._normal = D3DXVECTOR3(-1,1,-1);
_pData[1]._pos = D3DXVECTOR3(1, 1,-1); _pData[1]._normal = D3DXVECTOR3(1,1,-1);
_pData[2]._pos = D3DXVECTOR3(1,-1,-1); _pData[2]._normal = D3DXVECTOR3(1,-1,-1);
_pData[3]._pos = D3DXVECTOR3(-1,-1,-1);_pData[3]._normal = D3DXVECTOR3(-1,-1,-1);
_pData[4]._pos = D3DXVECTOR3(-1,1,1); _pData[4]._normal = D3DXVECTOR3(-1,1,1);
_pData[5]._pos = D3DXVECTOR3(1,1,1); _pData[5]._normal = D3DXVECTOR3(1,1,1);
_pData[6]._pos = D3DXVECTOR3(1,-1,1); _pData[6]._normal = D3DXVECTOR3(1,-1,1);
_pData[7]._pos = D3DXVECTOR3(-1, -1, 1);_pData[7]._normal = D3DXVECTOR3(-1,-1,1);
//Unlock the buffer
HR(m_pVertexBuffer->Unlock());
//Save the index number
WORD _indexNum = 12 * 3 ;
//Create the index buffer
HR(m_pDevice->CreateIndexBuffer(_indexNum * sizeof(WORD),D3DUSAGE_WRITEONLY,D3DFMT_INDEX16,
D3DPOOL_MANAGED,&m_pIndexBuffer,0));
//Lock the index buffer
WORD * _pData_Index = NULL ;
HR(m_pIndexBuffer->Lock(0,0,(void**)&_pData_Index,0));
//Set the index buffer
// Front face
_pData_Index[0] = 0 ; _pData_Index[1] = 1 ; _pData_Index[2] = 2 ;
_pData_Index[3] = 0 ; _pData_Index[4] = 2 ; _pData_Index[5] = 3 ;
//Back face
_pData_Index[6] = 5 ; _pData_Index[7] = 4 ; _pData_Index[8] = 7 ;
_pData_Index[9] = 5; _pData_Index[10] = 7 ; _pData_Index[11] = 6 ;
//Right face
_pData_Index[12] = 1 ; _pData_Index[13] = 5 ; _pData_Index[14] = 6 ;
_pData_Index[15] = 1 ; _pData_Index[16] = 6 ; _pData_Index[17] = 2 ;
//Left face
_pData_Index[18] = 4 ; _pData_Index[19] = 0 ; _pData_Index[20] = 3;
_pData_Index[21] = 4 ; _pData_Index[22] = 3 ; _pData_Index[23] = 7 ;
//Top face
_pData_Index[24] = 4 ; _pData_Index[25] = 5 ; _pData_Index[26] = 1 ;
_pData_Index[27] = 4 ; _pData_Index[28] = 1 ; _pData_Index[29] = 0 ;
//Bottom face
_pData_Index[30] = 6 ; _pData_Index[31] = 7 ; _pData_Index[32] = 3 ;
_pData_Index[33] = 6 ; _pData_Index[34] = 3 ; _pData_Index[35] = 2 ;
//Unlock the buffer
HR(m_pIndexBuffer->Unlock());
}
上面在指定顶点的法向向量的时候,没有使用公式进行计算了,而是自己手动的将最终结果写如进去,但是法向向量的原理还是一样的。
下面是绘制这个Cube的代码:
void CubeDemo::draw()
{
//Set vertex stream
HR(m_pDevice->SetStreamSource(0,m_pVertexBuffer,0,sizeof(VertexPN)));
//Set indice buffer
HR(m_pDevice->SetIndices(m_pIndexBuffer));
//Set the vertex declaration
HR(m_pDevice->SetVertexDeclaration(VertexPN::_vertexDecl));
//Create the world matrix
D3DXMATRIX _worldM ;
D3DXMatrixIdentity(&_worldM);
//Set the technique
HR(m_pEffect->SetTechnique(m_hTechnique));
//Set the gWVP matrix
D3DXHANDLE _hWVP = m_pEffect->GetParameterByName(0, "gWVP");
HR(m_pEffect->SetMatrix(_hWVP, &(_worldM* m_ViewMatrix* m_ProjMatrix)));
//Set the gInverseTranspose
D3DXMatrixInverse(&_worldM,NULL, &_worldM);
D3DXMatrixTranspose(&_worldM, &_worldM);
HR(m_pEffect->SetMatrix(m_gInverseTranspose, &_worldM));
//Set the gMaterial
HR(m_pEffect->SetVector(m_gMaterial,&D3DXVECTOR4(1, 0.5, 0.7, 0)));
//Set the gLightColor
HR(m_pEffect->SetVector(m_gLightColor, &D3DXVECTOR4(1, 0.8, 0.8, 0.8)));
//Set the gLightVector
HR(m_pEffect->SetVector(m_gLightVector, &D3DXVECTOR4(1,1,1,0)));
//Begin pass
UINT _pass = 0 ;
HR(m_pEffect->Begin(&_pass, 0));
HR(m_pEffect->BeginPass(0));
//Draw the primitive
HR(m_pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,8,0,12));
//End pass
HR(m_pEffect->EndPass());
HR(m_pEffect->End());
}
上面的很多参数,都是在Shader文件中需要使用的,本实例是使用Shader来进行编写的。下面是Shader的代码段:
//---------------------------------------------------------------------------
// declaration : Copyright (c), by XJ , 2014 . All right reserved .
// brief : This file will define the Diffuse shader.
// date : 2014 / 5 / 23
//----------------------------------------------------------------------------
uniform float4x4 gWVP ; //这个变量将会保存世界变换矩阵*相机变换矩阵*透视投影矩阵的积
//用这个矩阵,将点转化到裁剪空间中去
uniform float4x4 gInverseTranspose; //这个变量将会保存世界变换矩阵的逆矩阵*转置矩阵,用来对法向量进行变换
uniform float4 gMaterial; //这个变量用来保存顶点的材质属性,在本Demo中,将对所有的顶点使用相同的
//材质
uniform float4 gLightColor; //这个变量将用来保存一个平行光的颜色
uniform float3 gLightVector; //这个变量用来保存平行光的光照向量
//定义顶点着色的输入结构体
struct OutputVS
{
float4 posH : POSITION0 ;
float4 color : COLOR0 ;
};
OutputVS DiffuseVS(float3 posL: POSITION0, float3 normalL: NORMAL0)
{
//清空OutputVS
OutputVS outputVS = (OutputVS) 0 ;
//对顶点的法向向量进行变换
normalL = normalize(normalL);
float3 normalW = mul(float4(normalL, 0.0f),
gInverseTranspose).xyz;
normalW = normalize(normalW);
//根据漫反射公式:
// Color = max(L * Normal, 0)*(LightColor*Material)
float s = max(dot(gLightVector,normalW), 0);
outputVS.color.rgb = s*(gMaterial*gLightColor).rgb ;
outputVS.color.a = gMaterial.a ;
//使用gWVP将世界坐标转化为裁剪坐标
outputVS.posH = mul(float4(posL, 1.0f), gWVP);
//返回结果
return outputVS ;
}// end for Vertex Shader
float4 DiffusePS(float4 c: COLOR0): COLOR
{
return c ;
}// end for Pixel Shader
technique DiffuseTech
{
pass P0
{
vertexShader = compile vs_2_0 DiffuseVS();
pixelShader = compile ps_2_0 DiffusePS();
}
}
由于篇幅所限,这里无法列出所有的代码,只是列出了我认为比较重要的几段代码。
下面是最终的运行截图:
完整代码示例,可以在Diffuse_Demo下载。
好了,今天就到这里了,下次再见!!!