Light,Programming.Role.Playing.Games.with.DirectX书上把点光源,聚光灯,平行光和环境光都介绍了一下,案例也只给了最简单的平行光的源码,稍微简略了一点。其实用固定管线实现都很简单,就改几个参数,但用Shader来实现的话,就复杂了,涉及到了光照衰减公式等。还是老规则,左边的是用固定管线实现的,右边是用Shader实现的。
因为代码写的有点多,之前也有写过,所以就懒得再仔细写一遍。一个工程就包括了点光源,聚光灯,平行光3中实现,就如上图一样,通过1,2,3按键来控制。改shader比较影响效果,就只改了平行光的效果,没用lamber光照模型,用的是v社的halflamber光照模型,只计算diffuse漫反射,高光什么的再加进去,工程就有点大了,为了效果我已经加了在旋转的灯光模型和地表模型,都是用xfile加载的。
如果想看其他的(有高光的),可以下载源码去看下,里面我加了当前要讲的工程外,还加了4个工程,一个就是下图的点光源,另外3个就包含了Gouradu模型和phong模型的工程,聚光灯实现,平行光和半球模型和光照贴图的实现。
上图的代码其实很简单,为了玩,又加了边缘光的算法,就得出这种效果,有点怪怪的。
来看下渲染的代码,代码是在Draw3D上修改的:
固定管线:
void ModelClass::Render(IDirect3DDevice9* device, float time, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix, LightClass* light)
{
D3DXMATRIX xRotationMatrix, yRotationMatrix;
D3DXMATRIX translationMatrix;
LightType lightType;
D3DXVECTOR4 lightColor;
D3DXVECTOR4 lightPos;
D3DXVECTOR4 lightDir;
D3DXVECTOR4 lightAtten;
float phi, theta;
D3DLIGHT9 light9;
// Rotation
::D3DXMatrixRotationX(&xRotationMatrix, D3DX_PI * 0.25f);
::D3DXMatrixRotationY(&yRotationMatrix, -D3DX_PI * 0.25f);
// Translate
::D3DXMatrixTranslation(&translationMatrix, -2.0f, 5.0f, 0.0f);
::D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &xRotationMatrix);
::D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &yRotationMatrix);
::D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &translationMatrix);
device->SetTransform(D3DTS_WORLD, &worldMatrix);
device->SetTransform(D3DTS_VIEW, &viewMatrix);
device->SetTransform(D3DTS_PROJECTION, &projectionMatrix);
// Setup light
device->SetRenderState(D3DRS_LIGHTING, true);
lightType = light->GetLightType();
light->GetColor(lightColor);
light->GetPosition(lightPos);
light->GetDirection(lightDir);
light->GetAttenuation(lightAtten);
phi = light->GetPhi();
theta = light->GetTheta();
::ZeroMemory(&light9, sizeof(D3DLIGHT9));
switch (lightType)
{
case LightType::Point:
light9.Type = D3DLIGHT_POINT;
break;
case LightType::Spot:
light9.Type = D3DLIGHT_SPOT;
break;
case LightType::Directional:
light9.Type = D3DLIGHT_DIRECTIONAL;
break;
}
light9.Diffuse.r = lightColor.x;
light9.Diffuse.g = lightColor.y;
light9.Diffuse.b = lightColor.z;
light9.Diffuse.a = lightColor.w;
light9.Range = lightAtten.x;
light9.Attenuation0 = lightAtten.y;
light9.Attenuation1 = lightAtten.z;
light9.Attenuation2 = lightAtten.w;
light9.Position.x = lightPos.x;
light9.Position.y = lightPos.y;
light9.Position.z = lightPos.z;
light9.Direction.x = lightDir.x;
light9.Direction.y = lightDir.y;
light9.Direction.z = lightDir.z;
light9.Phi = phi;
light9.Theta = theta;
device->SetLight(0, &light9);
device->LightEnable(0, true);
device->SetStreamSource(0, m_vertexBuffer, 0, sizeof(VertexType));
device->SetFVF(VERTEX_FVF);
device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, m_vertexCount / 3);
}
所有灯光相关数据都存在lightclass中,lightclass头文件结构:
#pragma once
#include <d3d9.h>
#include <d3dx9.h>
#include "LightShaderClass.h"
#include "Utility.h"
class LightClass
{
public:
LightClass(LightType lightType);
LightClass(const LightClass& other);
~LightClass();
bool Initialize(IDirect3DDevice9* device, TCHAR* modelFilePath, D3DXVECTOR4 lightColor,
float range, float attenuation0, float attenuation1, float attenuation2, float phi, float theta);
void Shutdown();
void Render(IDirect3DDevice9* device, float time, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix);
void SetPosition(float x, float y, float z, float w);
LightType GetLightType();
void GetPosition(D3DXVECTOR4& position);
void GetDirection(D3DXVECTOR4& direction);
void GetColor(D3DXVECTOR4& color);
void GetAttenuation(D3DXVECTOR4& attenuation);
float GetPhi();
float GetTheta();
private:
bool InitializeMesh(IDirect3DDevice9* device, TCHAR* modelFilePath);
private:
ID3DXMesh* m_mesh;
LightShaderClass* m_lightShader;
LightType m_lightType;
D3DXVECTOR4 m_pos;
D3DXVECTOR4 m_color;
D3DXVECTOR4 m_atten;
float m_phi;
float m_theta;
};
固定管线的实现就简单,SetRenderState(D3DRS_LIGHTING, true)函数开启灯光,然后填充D3DLIGHT9结构,没了。具体的原理还是看shader代码会比较清楚,我就放shader后。
Shader渲染:
void ShaderModelClass::Render(IDirect3DDevice9* device, float time, D3DXMATRIX worldMatrix, D3DXMATRIX viewMatrix, D3DXMATRIX projectionMatrix, LightClass* light)
{
D3DXMATRIX xRotationMatrix, yRotationMatrix;
D3DXMATRIX translationMatrix;
D3DXMATRIX world2ObjectMatrix;
bool result;
UINT passMaxNum;
// Rotation
::D3DXMatrixRotationX(&xRotationMatrix, D3DX_PI * 0.25f);
::D3DXMatrixRotationY(&yRotationMatrix, D3DX_PI * 0.25f);
// Translate
::D3DXMatrixTranslation(&translationMatrix, 2.0f, 5.0f, 0.0f);
::D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &xRotationMatrix);
::D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &yRotationMatrix);
::D3DXMatrixMultiply(&worldMatrix, &worldMatrix, &translationMatrix);
::D3DXMatrixInverse(&world2ObjectMatrix, nullptr, &worldMatrix);
::D3DXMatrixTranspose(&world2ObjectMatrix, &world2ObjectMatrix);
result = m_colorShader->Render(device, time, worldMatrix, viewMatrix, projectionMatrix, world2ObjectMatrix, light);
if (!result)
return;
device->SetStreamSource(0, m_vertexBuffer, 0, sizeof(VertexType));
device->SetFVF(VERTEX_FVF);
m_colorShader->GetEffect()->Begin(&passMaxNum, 0);
for (UINT pass = 0; pass < passMaxNum; ++pass)
{
m_colorShader->GetEffect()->BeginPass(pass);
device->DrawPrimitive(D3DPT_TRIANGLELIST, 0, m_vertexCount / 3);
m_colorShader->GetEffect()->EndPass();
}
m_colorShader->GetEffect()->End();
}
shader渲染代码没什么大变化,还是具体看shader代码:
float time;
float4x4 worldMatrix;
float4x4 viewMatrix;
float4x4 projectionMatrix;
float4x4 world2ObjectMatrix;
float4 lightColor;
float4 lightPos;
float4 lightDir;
float4 lightAtten;
float phi;
float theta;
struct VertexInputType
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 diffuse : COLOR;
};
struct PixelInputType
{
float4 pos : POSITION;
float3 normal : TEXCOORD0;
float4 diffuse : TEXCOORD1;
float3 worldPos : TEXCOORD2;
};
PixelInputType ColorVertexShader(VertexInputType input)
{
PixelInputType output;
float4x4 worldViewMaxtrix = mul(worldMatrix, viewMatrix);
float4x4 worldViewProjectionMatrix = mul(worldViewMaxtrix, projectionMatrix);
output.pos = mul(input.vertex, worldViewProjectionMatrix);
output.normal = mul(input.normal, (float3x3)world2ObjectMatrix);
output.diffuse = input.diffuse;
output.worldPos = mul(input.vertex, worldMatrix).xyz;
return output;
}
float4 DirectionalLightPixelShader(PixelInputType input) : COLOR
{
float3 worldNormal = normalize(input.normal);
float3 worldLightDir = normalize(-lightDir.xyz);
float halfLambert = saturate(dot(worldNormal, worldLightDir)) * 0.5f + 0.5f;
float3 diffuse = input.diffuse.xyz * lightColor.xyz * halfLambert;
//float lambert = saturate(dot(worldNormal, worldLightDir));
//float3 diffuse = input.diffuse.xyz * lightColor.xyz * lambert;
float3 color = diffuse;
return float4(color, 1.0f);
}
float4 PointLightPixelShader(PixelInputType input) : COLOR
{
float3 worldNormal = normalize(input.normal);
float3 worldLightDir = normalize(lightPos.xyz - input.worldPos);
float distance = length(lightPos.xyz - input.worldPos);
float range = lightAtten.x;
float atten0 = lightAtten.y;
float atten1 = lightAtten.z;
float atten2 = lightAtten.w;
float3 diffuse = float3(0.0f, 0.0f, 0.0f);
if (distance < range)
{
diffuse = input.diffuse.xyz * lightColor.xyz * saturate(dot(worldNormal, worldLightDir));
float attenPara = 1.0f / (atten0 + atten1 * distance + atten2 * distance * distance);
diffuse *= attenPara;
}
float3 color = diffuse;
return float4(color, 1.0f);
}
float4 SpotLightPixelShader(PixelInputType input) : COLOR
{
float3 worldNormal = normalize(input.normal);
float3 worldLightDir = normalize(lightPos.xyz - input.worldPos);
float distance = length(lightPos.xyz - input.worldPos);
float range = lightAtten.x;
float atten0 = lightAtten.y;
float atten1 = lightAtten.z;
float atten2 = lightAtten.w;
float3 diffuse = float3(0.0f, 0.0f, 0.0f);
if (distance < range)
{
float tensity = saturate(dot(normalize(lightDir).xyz, -worldLightDir));
if (tensity >= phi)
{
tensity = (clamp(tensity, phi, theta) - phi) / (theta - phi) * 0.5f + 0.5f;
diffuse = input.diffuse.xyz * lightColor.xyz * saturate(dot(worldNormal, worldLightDir)) * tensity;
float attenParam = 1.0f / (atten0 + atten1 * distance + atten2 * distance * distance);
diffuse *= attenParam;
}
}
float3 color = diffuse;
return float4(color, 1.0f);
}
technique DirectionalLightTechnique
{
pass pass0
{
VertexShader = compile vs_2_0 ColorVertexShader();
PixelShader = compile ps_2_0 DirectionalLightPixelShader();
}
}
technique PointLightTechnique
{
pass pass0
{
VertexShader = compile vs_2_0 ColorVertexShader();
PixelShader = compile ps_2_0 PointLightPixelShader();
}
}
technique SpotLightTechnique
{
pass pass0
{
VertexShader = compile vs_2_0 ColorVertexShader();
PixelShader = compile ps_3_0 SpotLightPixelShader();
}
}
地表的渲染shader可右边Cube的几乎是一样的,不同处就在贴图上,所以也就不贴地表的shader实现了。平行光的shader代码是最简单,让我贴一个公式过来:
原理公式:diffuse = I*cosθ;
diffuse:反射光线的的光强;
I:入射光线的光强,方向如上图所示;
cosθ:入射光线和该顶点法线的余弦。
所以,最后的数学表达式为:diffuse = I*(L*N);也就是被我注释了代码。而所谓halflambert公式则是使得背光面不会很暗,从公式上就可以看出来,cosθ的值为(-1, 1)之间,调用saturate后值为(0, 1),所以背光面就会为黑色,而halflambert公式则使得值为(0.5, 1)之间,使得背光面有了默认的亮度。
重点开始,一开始学习DX9的固定管线的话,肯定不了解D3DLIGHT9中的衰减和聚光灯的phi及theta的用处,然后去查,发现理论讲的很清楚,但是就是不明白,直到看到shader代码后,其实真的很简单。
先说点光源的衰减和范围,其实就是在平行光计算的基础上乘一个衰减系数,而这个衰减系数的计算公式则是1/(att0+ ATT1* D+ ATT2* D* D),D是距离,也就是PointLightPixelShader代码中标红的那一段。
聚关灯的算法其实也可以简化为在点光源的基础上,乘一个角度衰减系数。也就是falloff参数,公式为((cosα-cosφ)/(cosθ-cosφ))falloff,就是聚光灯算法中标红的那一段,只是跟halflambert一样,把值限制在(0.5,1)之间而已,代码很清楚,就不细说了,看完代码,才能明白D3DLIGHT9中参数到底是如何参与运算的。具体的可以参考http://lengbingteng.iteye.com/blog/1767772这篇文章还有这篇多光源的http://lengbingteng.iteye.com/blog/1767772,也很不错,代码也很简单,可以仔细看下。
源码下载:下载地址