轻松愉快学Shader(1)——光照模型

来源:互联网 发布:王劲松演过的网络剧 编辑:程序博客网 时间:2024/06/05 20:24

  第一讲我们先用Shader实现一个最基础的光照模型。固定管线中的3D光照在DX9的龙书中已经介绍的很详细了,但是一旦换成可编程流水线,许多光照算法的实现都要用Shader自己来写,这部分的公式龙书并未详细给出,因此有必要在这里小讲一下。如果这些内容你之前已经掌握了,那本章就当作一个回顾吧,因为接下来的几章内容都会以此光照模型为基础。

光照的组成:

  我们都知道,D3D中把光分为环境光、漫反射光、镜面光。计算一个顶点的颜色,就是将这三种类型的光加起来。环境光是最简单的,只需要简单的加上去就可以了。漫反射光稍微复杂一些,它与光线的入射方向,以及顶点法线都有关系,计算公式如下:



衰减系数:k = saturate(cosθ) = saturate(dot(DirToLight, normal));

则顶点处的漫反射光颜色:DiffuseColor = LightColor *  k; 

  其中DirToLight是由顶点位置指向光源位置的向量,注意需要把它化成单位向量。normal是该顶点的法线向量,同样也是一个单位向量,角θ就是这两个单位向量之间的夹角。很显然,这个角越小,顶点能吸收的光的强度越强。这里,将光线的颜色,乘以cosθ,就得到了该顶点能接收的光的强度,而cosθ呢

?当然是等于两条单位向量的数量积了,这里dot函数就是计算两向量的数量积的函数
。注意, 当cos
θ小于0时,说明入射光线照射到了背面,此时该顶点是无法接收任何光线的,因此saturate函数的作用,就是如果cos
θ小于0的话,就简单的变成乘以0。相当于max(0, cos
θ).

  镜面光相比前两种光,其计算更加复杂。它不仅与光线方向和法线方向有光,而且与视点所在位置也有关系。如果能反射入我们眼睛的光线越多,当然我们看起来物体就越亮。镜面光的计算公式如下:



  图中的所有向量都已经化成了单位向量,其中点A就是顶点所在的位置。向量AS就是顶点位置指向光源位置的向量,即DirToLight,向量AE是从顶点位置指向视点位置的单位向量,AN就是顶点的法线,AB这条线比较特殊,它相当于∠EAS的角平分线,同样也是一个单位向量,它的计算方式并不复杂:

  AB = normalize(AE + AS)

  即将AE和AS两个单位向量相加,根据向量加法的平行四边形法则,则得到的就是∠EAS角平分线,然后调用normalize函数将其规范化。我们真正关系的是角平分线与顶点法线的夹角θ,即∠NAB,这个角越是小,就说明视点位置与反射光线的偏差越小,也就是越多的光线会射入眼睛。读者可以想象一下是不是这么一回事,当角θ等于0时,所有的反射光线都会射进眼睛。此时光的反射方向与AE方向重合了,此时A点的镜面光强度最大。

总结起来,A点的镜面反射光强度计算公式如下:

衰减系数: 

k = saturate(cosθ) = saturate(dot(AN, AB)) = saturate(dot(AN, normalize(AE + AS)))

则顶点处的镜面光颜色 :SpecularColor = LightColor * pow(k, Sharpness);

其中,为了让镜面光更加锐利,通常需要取cosθ的Sharpness次方。Sharpness的值越大,镜面光越锐利。


光源类型

  3D世界中的光源有三种:平行光、点光源、聚光灯。

  具体三种光源的性质,请各位参看龙书上的讲解。我们这里只讲如何在Shader中实现它们。本节中我们选取点光源作为我们的光源类型。因为平行光过于简单了,聚光灯又过于复杂了。出于学习的目的,我们这里先重点讲解点光源。点光源的特点是光线从光源处向四面八方扩散,而且随着光线传播距离增大,光的强度会逐渐减弱。因此要计算空间中某个位置接收的点光源的光线强度,通常要乘以一个衰减系数。它的计算公式如下:


  其中D是顶点到光源的距离,A0, A1, A2是表示光的常量、1次、2次衰减系数,这些值都是可以根据不同的情况进行指定的。


Shader算法实现:

  说了这么多,大家都该烦了,下面直接上HLSL代码。首先需要在Shader文件里定义如下变量:

matrix MatWVP; // 世界、观察、投影变换矩阵的乘积float4 AmbientLight = {0.0f, 0.0f, 0.1f, 1.0f}; //环境光float3 LightPos = {3.0f, 3.0f, -3.0f};   //点光源位置float4 Attenuation = {1.0f, 0.1f, 0.0f, 0.0f}; //点光源的衰减系数,分别存储在x,y,z分量里float4 DiffuseLight = {0.0f, 0.0f, 1.0f, 1.0f};   //漫反射光float4 SpecularLight = {1.0f, 1.0f, 1.0f, 1.0f};   //镜面光float4 ViewPos; //视点位置struct VS_OUTPUT{float4 Position : POSITION0;float4 Color : COLOR0;};
  这些变量和声明都不难理解,其中MatWVP和ViewPos两个变量是需要在DX9程序中传入的,因此在Shader文件中就没有初始化。VS_OUTPUT是顶点着色器需要返回的类型。

  下面我们自定义了一个函数,也是最核心的功能——计算某个顶点的颜色:

//计算某个顶点的颜色float4 CalculateLightColor(float4 Pos, float3 normal){//从顶点到光源的向量float3 LightDir = LightPos - Pos;//顶点与光源之间的距离float Dist = length(LightDir);//将LightDir向量化成单位向量LightDir = LightDir / Dist;//计算点光源的衰减系数,距离越大,光就越弱float DistAtten = 1.0f / (Attenuation.x + Attenuation.y * Dist + Attenuation.z * Dist * Dist);//计算漫反射光的衰减系数float AngleAtten = saturate(dot(LightDir, normal));//顶点到视点的向量float3 EyeDir = normalize(ViewPos.xyz - Pos.xyz);//上图中的角平分线,即向量ABfloat3 HalfVect = normalize(LightDir + EyeDir);//计算镜面光的衰减系数float SpecAtten = saturate(dot(normal, HalfVect));//最终顶点处的颜色,这里指定Sharpness的值是32return DistAtten * (DiffuseLight * AngleAtten + SpecularLight * pow(SpecAtten, 32));}

  很显然,这个CalculateLightColor函数根据传入顶点位置和法线向量,计算该顶点的光照颜色。有的读者会注意到这里似乎忘了加上环境光(AmbientLight),其实没关系,由于环境光通常无需差值处理,因此我们在像素着色器里再加上。

下面就是该Shader的顶点着色器和像素着色器的代码:

VS_OUTPUT vs_main(float4 Pos : POSITION0, float3 normal : NORMAL){VS_OUTPUT output = (VS_OUTPUT)0;output.Position = mul(Pos, MatWVP);output.Color = CalculateLightColor(Pos, normal);return output;}float4 ps_main(VS_OUTPUT input) : COLOR0{return input.Color + AmbientLight;}

  是不是非常简单呢?在顶点着色器里,我们只需要根据MatWVP矩阵变换顶点位置,同时计算该顶点处的光照颜色。在像素着色器里,读取到的某像素颜色是根据邻近顶点进行差值处理后的值,该值仅仅包含漫反射光和镜面光,因此需要加上环境光颜色。

源代码欣赏:

  在本节的demo中,我们在主程序中画了一个球体,并用点光源对它进行照射。下面时C++部分的代码:

#include <d3d9.h>#include <d3dx9.h>#pragma comment(lib,"d3d9.lib")  #pragma comment(lib,"d3dx9.lib")  #pragma comment(lib,"dxguid.lib")  #pragma comment(lib, "winmm.lib")#define SCREEN_WIDTH  800#define SCREEN_HEIGHT 600#define SAFE_RELEASE(p) {if(p){(p)->Release(); (p)=NULL;}}LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);bool Game_Init(HWND hwnd);void Game_Update(float);void Game_Render();void Game_Exit();LPDIRECT3DDEVICE9 g_pd3dDevice = NULL;LPD3DXMESH g_pSphere = NULL; //球体LPD3DXEFFECT g_pEffect = NULL; //效果框架int WINAPI WinMain (HINSTANCE hInstance,                    HINSTANCE hPrevInstance,                    LPSTR lpszArgument,                    int iCmdShow){    HWND hwnd;     MSG msg;     WNDCLASS wndclass; static TCHAR szClassName[ ] = TEXT("WindowsApp");    /* 第一步:注册窗口类 */    wndclass.hInstance = hInstance;    wndclass.lpszClassName = szClassName;    wndclass.lpfnWndProc = WindowProcedure;     wndclass.style = CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS;     wndclass.cbWndExtra = 0;wndclass.cbClsExtra = 0;wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);wndclass.lpszMenuName = NULL;    if (!RegisterClass (&wndclass))        return 0;RECT rc = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, false);    /* 第二步:创建窗口 */hwnd = CreateWindow(szClassName, TEXT("MyApp"), WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT, rc.right - rc.left,   rc.bottom - rc.top,NULL,NULL,hInstance,NULL);    /* 第三步:显示窗口 */    ShowWindow (hwnd, iCmdShow);UpdateWindow(hwnd);Game_Init(hwnd);float lastTime = timeGetTime() * 0.001f;float currentTime, delta;    /* 第四步:消息循环 */while(true){if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)){if(msg.message == WM_QUIT)break;TranslateMessage(&msg);DispatchMessage(&msg);}else {currentTime = timeGetTime() * 0.001f;delta = currentTime - lastTime;lastTime = currentTime;Game_Update(delta);Game_Render();}}Game_Exit();return msg.wParam;}/*窗口过程*/LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam){    switch (message)      {case WM_DESTROY:PostQuitMessage (0); break;default:return DefWindowProc (hwnd, message, wParam, lParam);    }    return 0;}bool Game_Init(HWND hwnd){/* D3D初始化 */LPDIRECT3D9 d3d9 = Direct3DCreate9(D3D_SDK_VERSION);D3DCAPS9 d3dcap;d3d9->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &d3dcap);DWORD vp;if( d3dcap.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT )vp = D3DCREATE_HARDWARE_VERTEXPROCESSING;elsevp = D3DCREATE_SOFTWARE_VERTEXPROCESSING;D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp));d3dpp.BackBufferWidth            = SCREEN_WIDTH;d3dpp.BackBufferHeight           = SCREEN_HEIGHT;d3dpp.BackBufferFormat           = D3DFMT_A8R8G8B8;d3dpp.BackBufferCount            = 1;d3dpp.MultiSampleType            = D3DMULTISAMPLE_NONE;d3dpp.MultiSampleQuality         = 0;d3dpp.SwapEffect                 = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow              = hwnd;d3dpp.Windowed                   = true;d3dpp.EnableAutoDepthStencil     = true; d3dpp.AutoDepthStencilFormat     = D3DFMT_D24S8;d3dpp.Flags                      = 0;d3dpp.FullScreen_RefreshRateInHz = 0;d3dpp.PresentationInterval       = D3DPRESENT_INTERVAL_IMMEDIATE;HRESULT hr;hr = d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice);d3d9->Release();if(FAILED(hr))return false;//创建球体网格, 球体半径为1.0D3DXCreateSphere(g_pd3dDevice, 1.0f, 20, 20, &g_pSphere, NULL);//初始化效果框架LPD3DXBUFFER pCompilerError;hr = D3DXCreateEffectFromFileA(g_pd3dDevice, "effect.fx", NULL, NULL,D3DXSHADER_DEBUG, NULL, &g_pEffect, &pCompilerError);if(FAILED(hr)){if(pCompilerError){MessageBoxA(NULL, (char*)pCompilerError->GetBufferPointer(), NULL, 0);pCompilerError->Release();}return false;}D3DXHANDLE hTech = g_pEffect->GetTechniqueByName("T");g_pEffect->SetTechnique(hTech);return true;}void Game_Update(float delta){/* 计算观察矩阵和投影矩阵,并传入Shader */D3DXMATRIX matView;D3DXVECTOR3 vEye(0.0f, 0.0f, -5.0f);D3DXVECTOR3 vAt(0.0f, 0.0f, 0.0f); D3DXVECTOR3 vUp(0.0f, 1.0f, 0.0f);D3DXMatrixLookAtLH(&matView, &vEye, &vAt, &vUp);D3DXMATRIX matProj; D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4.0f, (float)SCREEN_WIDTH / (float)SCREEN_HEIGHT, 1.0f, 1000.0f); D3DXHANDLE hMatWVP = g_pEffect->GetParameterByName(NULL, "MatWVP");g_pEffect->SetMatrix(hMatWVP, &(matView * matProj));//将视点位置传入ShaderD3DXHANDLE hViewPos = g_pEffect->GetParameterByName(NULL, "ViewPos");g_pEffect->SetVector(hViewPos, &D3DXVECTOR4(vEye.x, vEye.y, vEye.z, 1.0f));}void Game_Render(){g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0); g_pd3dDevice->BeginScene();g_pEffect->Begin(0, 0);g_pEffect->BeginPass(0);g_pSphere->DrawSubset(0);g_pEffect->EndPass();g_pEffect->End();g_pd3dDevice->EndScene();g_pd3dDevice->Present(NULL, NULL, NULL, NULL);}void Game_Exit(){SAFE_RELEASE(g_pSphere)SAFE_RELEASE(g_pEffect)SAFE_RELEASE(g_pd3dDevice);}

效果文件effect.fx中的代码如下,和刚才讲的没什么不同:


matrix MatWVP; // 世界、观察、投影变换矩阵的乘积float4 AmbientLight = {0.0f, 0.0f, 0.1f, 1.0f}; //环境光float3 LightPos = {3.0f, 3.0f, -3.0f};   //点光源位置float4 Attenuation = {0.5f, 0.1f, 0.0f, 0.0f}; //点光源的衰减系数,分别存储在x,y,z分量里float4 DiffuseLight = {0.0f, 0.0f, 1.0f, 1.0f};   //漫反射光float4 SpecularLight = {1.0f, 1.0f, 1.0f, 1.0f};   //镜面光float4 ViewPos; //视点位置struct VS_OUTPUT{float4 Position : POSITION0;float4 Color : COLOR0;};//计算某个顶点的颜色float4 CalculateLightColor(float4 Pos, float3 normal){//从顶点到光源的向量float3 LightDir = LightPos - Pos;//顶点与光源之间的距离float Dist = length(LightDir);//将LightDir向量化成单位向量LightDir = LightDir / Dist;//计算点光源的衰减系数,距离越大,光就越弱float DistAtten = 1.0f / (Attenuation.x + Attenuation.y * Dist + Attenuation.z * Dist * Dist);//计算漫反射光的衰减系数float AngleAtten = saturate(dot(LightDir, normal));//顶点到视点的向量float3 EyeDir = normalize(ViewPos.xyz - Pos.xyz);//上图中的角平分线,即向量ABfloat3 HalfVect = normalize(LightDir + EyeDir);//计算镜面光的衰减系数float SpecAtten = saturate(dot(normal, HalfVect));//最终顶点处的颜色,这里指定Sharpness的值是32return DistAtten * (DiffuseLight * AngleAtten + SpecularLight * pow(SpecAtten, 32));}VS_OUTPUT vs_main(float4 Pos : POSITION0, float3 normal : NORMAL){VS_OUTPUT output = (VS_OUTPUT)0;output.Position = mul(Pos, MatWVP);output.Color = CalculateLightColor(Pos, normal);return output;}float4 ps_main(VS_OUTPUT input) : COLOR0{return input.Color + AmbientLight;}technique T{pass P0{Lighting = false;VertexShader = compile vs_2_0 vs_main();PixelShader = compile ps_2_0 ps_main();}}

程序的运行截图:



本节源代码下载请点击这里


原创粉丝点击