DirectX 9.0 (3) 漫射光

本文介绍了计算机图形学中的漫射光照模型,讲解了物体颜色的形成原理,平行光、漫反射的概念,以及顶点法向向量的计算。还探讨了法向向量的变换和漫射光模型的数学表达式,最后给出了实现漫射光照的代码示例。
摘要由CSDN通过智能技术生成

引言

        在计算机图形中,为了使的图形看上去具有立体的效果,我们需要为我们的3D物体,加上一点光照效果。如果你学过绘画的课程,就会知道,一个物体,如何想要画的具有立体的感觉,我们需要对物体进行光照处理。比如说,我们要给物体画上暗部,亮部,高光部分,反光部分,还有中间的光暗过度区部分。画家是通过自己的感觉和艺术修养来对物体进行光照处理,而我们程序员有我们自己的方法。虽然我们没有那么好的美术功底,但是,我们可以通过建立光照模型,对物体进行着色,通过精确的模型对物体进行着色,要比画家绘制的立体物体要更加的真实点,但是会缺少相应的艺术感。如果你想要一个艺术话的光照模型,可以使用catton着色效果来对物体进行着色,很多著名的游戏如《无主之地》系列等就是使用catton着色的艺术效果来制作的。

      好了,在接下来的文章中,将会向大家讲述如何搭建一个比较真实的光照模型。


物体为什么会有颜色?

        这个缤纷的世界,充满了各种各样的色彩,但是,读者您有没有想过,为什么物体会具有这样或者那样的色彩了?

         上帝在创造世界的时候,给予了光,和人类感受光的能力。物体的颜色也是通过光和物体本身的交互,再加上人类对光的感受能力,才形成的。在这里,不会和大家讲述很详细的形成过程,那要涉及大量我自己都没有掌握的知识,所以,这里只是简要的介绍下,颜色是如何形成的。

         一个物体,它具有对光进行吸收和反射的能力。这种能力在图形学中,我们使用材质属性来表示。材质属性,定义了它会对光源进行什么样的吸收和反射。不同的材质,具有不同的特性。大家都知道,在计算机中表示颜色一般是使用RGB的方式,用R 表示Red红色, G表示Green 绿色, B表示Blue蓝色。我们同样的,可以使用这样的颜色值来定义一个光源的颜色,从而在通过材质属性,对不同的分量,进行不同的吸收和反射。这样就可以模拟出一个基本的物体和光交互的方法。

      但是,读者可能会想到,这只是,物体和光之间进行交互的方式罢了。那么为什么我们能够看到了?

     人类的眼睛很奇妙,它的内部有很多很多微小的感应神经。这些神经元对光的强度很敏感,所以,当光照射到人眼中之后,这些传感器就会接受到不同强度的光照射,然后经过一些复杂的生物变化,形成信号,发送到了我们的大脑,大脑根据这些信号来形成一个图像,那就是我们看到的东西。

     所以,与其说物体具有颜色,还不如说,是物体给予我们眼睛某种颜色的感觉。通过上面的描述,我们也会发现,材质定义的巧妙之处。材质定义了,物体在光源照射下,将会反射什么样的颜色,如果这样的颜色被我们的眼睛接受到,那么,我们就会觉得这个物体就是它反射的颜色。

    所以,通过人眼,材质,和光源属性,我们就能够模拟出一个简单的光照模型出来。


平行光

       在这里,先介绍一个最基本的光源,平行光。

      一般来说,我们认为太阳,照射到地球表面上一个很小的面积时,太阳光是平行的照射过来的,所以这样的光我们称之为平行光。


漫反射

     当平行光,照射到一个表面粗糙的物体上的时候,由于表面很是粗糙,光线会朝着不同的方向进行反射。而一个物体表面的粗糙程度要复杂的多,所以,我们大可以认为,粗糙表面的物体,会将光线反射到任何方向上去。所以,只要视线不被遮挡,我们的眼睛总是能够看到这样的物体。而表面非常光滑的物体,可能会将光线反射到某一个特定范围上的方向,比如玻璃,这也就是为什么经常有粗心的人会撞到玻璃上去,因为他的眼睛没有接受到玻璃反射过来的光源,自然就不能够看到了。不过,实际上,绝对光滑的物体是不存在的,否则,这样利用光线进行反射,说不定,可以做出隐身衣哦>_<


顶点的法向向量

      好了,上面闲话已经说了很多了,现在来写理论的东西。我们都知道,在3D图形学中,我们使用顶点来表示多边形,然后利用多边形构建出物体。为了能够进行光照处理,我们需要知道每个顶点的法向向量。如果读者不知道什么是法向向量,可以参考下高中的物理或者几何数学。这里不再赘述。

     对于一个平面来说,它的法向向量是唯一的,这个法向向量垂直于这个平面上任何两个不重合点的连线。额,读者可能觉得,这样的向量有两个,那到底是哪一个了???

    的确,这样的向量的确有两个,要确定一个平面的法向向量,我们还需要知道,这个平面上的点是通过什么样的绕序(winding order)来定义的。在DirectX中,我们是使用顺时针的绕序方式来定义一个多边形的。(这是基本的DirectX知识,如果读者对这些知识,不甚了解的话,可以在CSDN中搜索浅墨,在他的博文中,会详细的讲述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下载。


好了,今天就到这里了,下次再见!!!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值