上文我们小小的练习了下点光源的实现,理解phong式光照模型,本文自然就是讲解聚光灯的,并实现多个聚光灯,给它们加点控制。
聚光灯与点光源有所不同,其有一个聚束范围,一个方向,在范围外的我们认为不受聚光灯的影响,其粗略的模型公式就是计算光线与光源设定的中心方向的夹角,当其超出一个范围值,我们便将其的漫反射颜色和镜面反射颜色设为0;
超级简单的,利用单位向量d和-l的点乘就能计算出夹角的余弦值,设置好余弦值就可以控制角度。
那么同上文,我们要给出聚光灯的结构体,去描述聚光灯的行为属性,你可以理解为我们将数据抽象为聚光灯,亦可理解为我们将聚光灯的基本构造实例化为数据。
HLSL:
struct SpotlLight
{
float4 Ambient;
float4 Diffuse;
float4 Specular;
float3 Position;
float Range;
float3 Direction;
float spot;
float3 Att;//attenuation
float Pad;
};
对应的C++结构体:
struct Light_desc
{
Light_desc(){ ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Position;
float Range;
XMFLOAT3 Direction;
float spot;
XMFLOAT3 Att;//attenuation
float Pad;
};
其中spot是用来控制聚光灯的圆锥体区域大小,默认大于1,其实就是 -L 和 d 的夹角余弦值的幂指数s。
当spot越大,余弦值的变化也就越明显,自然聚光灯的高亮范围就越小,但是整体亮度却变低了,不符合我们把聚光灯放近一点的特性,故我们在原龙书的基础上加了点改变,
color += color*saturate(Ambient_Color + Diffuse_Color + Specular_Color)* g_SpotLight[i].Spot/2;
我们给最终颜色乘上一个spot的正比例,来抵消余弦次幂带来的不切实际的变化,达到增加亮度的效果,下面为效果图:
spot为3
spot为8
void Light::Init_Light()
{
Light_Desc.Ambient = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
Light_Desc.Diffuse = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
Light_Desc.Specular = XMFLOAT4(0.6f, 0.6f, 0.8f, 1.0f);
Light_Desc.Position = XMFLOAT3(-2.0f, 0.4f, 0.4f);
Light_Desc.Range = float(20.f);
Light_Desc.Direction = XMFLOAT3(-0.0f, -1.0f, -1.0f);
Light_Desc.spot = float(4.0f);
Light_Desc.Att = XMFLOAT3(0.01f, 0.01f, 0.01f);
Light_Desc.Pad = 0;
}
非常简单,我在Light里面就可以初始化数据了,在这里初始化并创建我们这个灯光实例不对的,但是就一个灯光,我管它呢,我懒得去其它地方赋值,
void Light::Deploy_Shader(ID3DX11Effect* effect)
{
m_fxLight = effect->GetVariableBySemantic("SPOTLIGHT");
}
void Light::Frame_Light()
{
m_vCameraPosition = D3DXVECTOR3(Light_Desc.Position.x, Light_Desc.Position.y, Light_Desc.Position.z);
m_vLookVector = D3DXVECTOR3(Light_Desc.Direction.x, Light_Desc.Direction.y, Light_Desc.Direction.z);
D3DXVec3Normalize(&m_vLookVector, &m_vLookVector);
//正交并规范化m_vRightVector
D3DXVec3Cross(&m_vRightVector, &m_vUpVector, &m_vLookVector);
D3DXVec3Normalize(&m_vRightVector, &m_vRightVector);
//正交并规范化m_vUpVector
D3DXVec3Cross(&m_vUpVector, &m_vLookVector, &m_vRightVector);
m_fxLight->SetRawValue(&Light_Desc, 0, sizeof(Light_Desc));
}
注意,我在Frame中给light添加了一个控制向量正交化,
其实就是把camera的代码复制过来而已:
void Light::Rotate_RightVector(float fAngle)
{
D3DXMATRIX R;
//D3DXMatrixRotationX(&R, fAngle);
D3DXMatrixRotationAxis(&R, &m_vRightVector, fAngle);//创建出绕m_vRightVector旋转fAngle个角度的R矩阵
D3DXVec3TransformCoord(&m_vUpVector, &m_vUpVector, &R);//让m_vUpVector向量绕m_vRightVector旋转fAngle个角度
D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vRightVector旋转fAngle个角度
Light_Desc.Direction = XMFLOAT3(m_vLookVector.x, m_vLookVector.y, m_vLookVector.z);
}
void Light::Rotate_UpVector(float fAngle)
{
D3DXMATRIX R;
D3DXMatrixRotationAxis(&R, &m_vUpVector, fAngle);//创建出绕m_vUpVector旋转fAngle个角度的R矩阵
//D3DXMatrixRotationY(&R, fAngle);
D3DXVec3TransformCoord(&m_vRightVector, &m_vRightVector, &R);//让m_vRightVector向量绕m_vUpVector旋转fAngle个角度
D3DXVec3TransformCoord(&m_vLookVector, &m_vLookVector, &R);//让m_vLookVector向量绕m_vUpVector旋转fAngle个角度
Light_Desc.Direction = XMFLOAT3(m_vLookVector.x, m_vLookVector.y, m_vLookVector.z);
}
本着嫌少不嫌多的原则,我也把其控制代码贴出来,具体可以参见源码的Light.cpp;
BOOL graphicsclass::Init_Graphics()
{
m_light = new Light;
m_light->Init_Light();
m_light->Deploy_Shader(m_shader->GetIeffect());
}
BOOL graphicsclass::Frame_Graphics()
{
m_light->Frame_Light();
}
Light* light = graphics_class->Ilight();
if (input_class->IsRightMouseButtonDown(0))
{
light->Rotate_UpVector(input_class->MouseDX()*-0.003f);
light->Rotate_RightVector(input_class->MouseDY()*-0.006f);
}
void graphicsclass::Clean_Graphics()
{
m_light->Clean_Light();
delete m_light; m_light = NULL;
}
至此,我们C++基本构造就已经完成。接下来就是简单的HLSL配置;
跟点光源区别不大,加点东西就行。
影子量:常量buff
#include "Lighthead.fx"
Texture2D ShaderTexture; //纹理资源
cbuffer cbPerFrame
{
SpotlLight g_SpotLight:SPOTLIGHT;
}
cbuffer cbPerObject
{
float4x4 g_worldViewProj : WORLDVIEWPROJECTION;
float4x4 g_world : WORLD;
float4x4 g_worldInvTranspose : WORLDINVTRANSPOSE;
}
顶点:
struct VertexIn
{
float3 Pos:POSITION;
float3 Normal:NORMAL;
float2 Tex:TEXCOORD0;
};
struct VertexOut
{
float4 PosH :SV_POSITION;
float3 PosW :POSITION;
float3 Normal :NORMAL;
float2 Tex :TEXCOORD0;
float4 DiffuseColor : TEXCOORD1;
float4 SpecularColor : TEXCOORD2;
float3 lightVec :TEXCOORD3;
};
这里我将一些数据放在VertexOut下的纹理数据组里面,并没有什么特殊意义,就是想用用看,其实跟你定义个临时变量差不多,看您随意,以后我们会学习到纹理数组更多的用途,现在把它看为容器就行。
顶点变化函数:
VertexOut VS(VertexIn In )
{
VertexOut Out = (VertexOut)0;
Out.PosH = mul(float4(In.Pos, 1.0f), g_worldViewProj);
float4 posW = mul(float4(In.Pos, 1.0f), g_world);
Out.PosW = posW.xyz;
Out.Normal = mul(In.Normal, (float3x3)g_worldInvTranspose);
Out.Normal = normalize(Out.Normal);
Out.Tex = In.Tex;
Out.DiffuseColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
Out.SpecularColor = float4(0.0f, 0.0f, 0.0f, 0.0f);
Out.lightVec = g_SpotLight.Position - Out.PosW;
return Out;
}
没什么新的变化,很容易理解,这里我计算lightVec 主要是不想在像素着色器中计算,这里计算,然后光栅化插值,会好那么一点点。
像素着色函数:
这里跟点光源的处理相同
float4 PS_02(VertexOut In) :SV_Target
{
float4 Texcolor;
float d = length(In.lightVec);
float4 Ambient = g_SpotLight.Ambient;
Texcolor = ShaderTexture.Sample(samTriLinear, In.Tex);
if (d > g_SpotLight.Range)
{
return Texcolor*(In.DiffuseColor + In.SpecularColor + Ambient);//saturate
}
In.lightVec /= d;
float diffuseFactor = dot(In.lightVec, In.Normal);
float SpecularFactor = float(0.7f);
float3 CameraPosition = float3(0.f, 5.0f, -5.0f);
float3 toEye = CameraPosition - In.PosW;
[flatten]
if (diffuseFactor>0.0f)
{
float3 v = reflect(-In.lightVec, In.Normal);
float specFactor = pow(max(dot(v, toEye), 0.0f), SpecularFactor);
In.DiffuseColor = diffuseFactor*g_SpotLight.Diffuse;
In.SpecularColor = SpecularFactor*g_SpotLight.Specular;
}
float3 dir = normalize(g_SpotLight.Direction);
float spot= max(dot(-In.lightVec, dir), 0.0f);
float min = 0.85f;
if (spot>= min)
{
float spot = pow(fTensity, g_SpotLight.spot);
float Att = spot / dot(g_SpotLight.Att, d); /*float3(1.0f, d, d*d)*/
Ambient *= spot;
In.DiffuseColor *= Att * 30 * spot;
In.SpecularColor *= Att * 30 * spot;
}
else
{
In.DiffuseColor = 0;
In.SpecularColor = 0;
}
float4 color;
color = Texcolor*saturate(In.DiffuseColor + In.SpecularColor + Ambient)*g_SpotLight.spot/2;//saturate
return color;
}
值得注意的是,我这里并没有使用龙书的代码,而是采用简单的边界控制实现聚光灯,效果如下图:
好像还不错,但是边界像素颗粒比较严重,所以我们设置一个过渡区去优化。
float min = 0.85f;
float max = 1.0f;
if (spot >= min)
{
float fTensity = spot;
float spot = pow(spot , g_SpotLight.spot);
float Att = spot / dot(g_SpotLight.Att, d); /*float3(1.0f, d, d*d)*/
Ambient *= spot;
fTensity = clamp(fTensity, min, max) - min;
In.DiffuseColor *= Att*fTensity * 30 * spot;
In.SpecularColor *= Att*fTensity * 30 * spot;
}
clamp函数返回min至max中的一个值,这个函数是什么意思了,就是fTensity大于MAX就等于MAX,小于MIN就等于MIN,效果如下图:
还不错,这样我们对灯光的运用就很熟练了,所以我们可以来点新的花样:
实现代码:
float X_Distance = abs(In.PosW.x);
float Z_Distance = abs(In.PosW.z);
float MIN = 0.8f;
float MAX = 1.8f;
if (X_Distance <= MIN || Z_Distance <= MIN)
{
In.DiffuseColor *= 0.6f;
In.SpecularColor *= 0.6f;
}
else if (X_Distance >= MAX && Z_Distance >= MAX)
{
In.DiffuseColor *= 0.6f;
In.SpecularColor *= 0.6f;
}
else
{
In.DiffuseColor = 0;
In.SpecularColor = 0;
}
float Att = 1.0f / dot(g_SpotLight.Att, d);
In.DiffuseColor *= Att;
In.SpecularColor *= Att;
float4 color;
color = Texcolor*saturate(In.DiffuseColor + In.SpecularColor + Ambient)*g_SpotLight.spot / 3;//saturate
return color;
}
很简单吧,但是特效可是十足的,稍微改动一下代码:
else if (X_Distance >= MAX && Z_Distance >= MAX)
||
||
___||___
\ /
\ /
\ /
\/
else if (X_Distance >= MAX || Z_Distance >= MAX)
效果又不一样:
是不是很令人心动,更多特效等待您的发掘。
本例我们实现四个聚光灯,相应成彰,首先给light.cpp来个扩建。
class Light
{
Light_desc* Light_Desc;
}
头文件很简单的改变,创建灯光结构体指针。
完整文件:
#pragma once
#include "Util.h"
class Light
{
public:
Light(int light_num);
~ Light();
private:
struct Light_desc
{
Light_desc(){ ZeroMemory(this, sizeof(this)); }
XMFLOAT4 Ambient;
XMFLOAT4 Diffuse;
XMFLOAT4 Specular;
XMFLOAT3 Position;
float Range;
XMFLOAT3 Direction;
float spot;
XMFLOAT3 Att;//attenuation
float Pad;
};
private:
D3DXVECTOR3* m_vRightVector;// 右分量向量
D3DXVECTOR3* m_vUpVector; //上分量向量
D3DXVECTOR3* m_vLookVector; // 观察方向向量
D3DXVECTOR3* m_vLightPosition; // 光源位置
private:
ID3DX11EffectVariable *m_fxLight;
size_t m_lightNum;
public:
Light_desc* Light_Desc;
void Init_Light();
void Deploy_Shader(ID3DX11Effect*);
void Frame_Light();
void Clean_Light();
void Rotate_RightVector(float);
void Rotate_UpVector(float);
};
light.cpp:很简单的改变,就是将单例改为数组。
构造析构:
Light::Light(int light_num)
{
m_lightNum = light_num;
m_fxLight = NULL;
Light_Desc = NULL;
}
Light::~Light()
{
m_lightNum = 0;
m_fxLight = NULL;
Light_Desc = NULL;
}
初始化:
void Light::Init_Light()
{
Light_Desc = new Light_desc[m_lightNum];
m_vRightVector = new D3DXVECTOR3[m_lightNum];
m_vUpVector = new D3DXVECTOR3[m_lightNum];
m_vLookVector = new D3DXVECTOR3[m_lightNum];
m_vLightPosition = new D3DXVECTOR3[m_lightNum];
for (size_t i = 0; i < m_lightNum; i++)
{
m_vRightVector[i] = D3DXVECTOR3(1.f, 0.f, 0.f);// 右分量向量
m_vUpVector[i] = D3DXVECTOR3(0.f, 1.f, 0.f);// 上分量向量
m_vLookVector[i] = D3DXVECTOR3(0.0f, 0.0f, 1.0f); // 观察方向向量
m_vLightPosition[i] = D3DXVECTOR3(0.0f, 0.0f, 0.0f);// 光源位置
}
}
配置shader文件:
void Light::Deploy_Shader(ID3DX11Effect* effect)
{
m_fxLight = effect->GetVariableByName("g_SpotLight");
}
循环部分:
void Light::Frame_Light()
{
for (size_t i = 0; i < m_lightNum; i++)
{
// m_vLightPosition[i] = D3DXVECTOR3(Light_Desc[i].Position.x, Light_Desc[i].Position.y, Light_Desc[i].Position.z);
m_vLookVector[i] = D3DXVECTOR3(Light_Desc[i].Direction.x, Light_Desc[i].Direction.y, Light_Desc[i].Direction.z);
D3DXVec3Normalize(&m_vLookVector[i], &m_vLookVector[i]);
//正交并规范化m_vRightVector
D3DXVec3Cross(&m_vRightVector[i], &m_vUpVector[i], &m_vLookVector[i]);
D3DXVec3Normalize(&m_vRightVector[i], &m_vRightVector[i]);
//正交并规范化m_vUpVector
D3DXVec3Cross(&m_vUpVector[i], &m_vLookVector[i], &m_vRightVector[i]);
}
m_fxLight->SetRawValue(Light_Desc,0, m_lightNum* sizeof(Light_desc));
}
控制部分和回收资源部分就不贴代码了,很简单的重复工作,
读者看到这里请注意,本例函数返回值大部分为void,这并不值得
提倡,在重要且容易出错的函数内,应实现一次或多次判断,避免函数
在错误的道路上越走越远,那样你去寻找错误来源时就不会很伤脑筋。
在灯光的上级抽象类中给聚光灯赋值:
void graphicsclass::Create_Mutil_Light(int light_num)
{
m_light = new Light(light_num);
m_light->Init_Light();
for (size_t i = 0; i < light_num; i++)
{
m_light->Light_Desc[i].Ambient =XMFLOAT4(0.1f, 0.1f, 0.1f, 1.0f);
m_light->Light_Desc[i].Diffuse = XMFLOAT4(0.0f, 0.0f, 0.0f, 1.0f);
m_light->Light_Desc[i].Specular = XMFLOAT4(0.4f, 0.4f, 0.4f, 1.0f);
m_light->Light_Desc[i].Range = float(20.f);
m_light->Light_Desc[i].spot = float(8.0f);
m_light->Light_Desc[i].Att = XMFLOAT3(0.1f, 0.1f, 0.1f);
m_light->Light_Desc[i].Pad = (float)i;
m_light->Light_Desc[i].Direction = XMFLOAT3(0.0f, -1.0f, -1.0f);
XMVECTOR dir= XMVector3Normalize(XMLoadFloat3(&m_light->Light_Desc[i].Direction));
XMStoreFloat3(&m_light->Light_Desc[i].Direction, dir);
//m_light[i].Light_Desc.Light_Contribution = XMFLOAT4(0,0,0,0);
m_light->Deploy_Shader(m_shader->GetIeffect());
}
m_light->Light_Desc[0].Position = XMFLOAT3(4.0f, 0.8f, -4.0f);
m_light->Light_Desc[0].Diffuse = XMFLOAT4(0.2f, 0.2f, 0.1f, 1.0f);
m_light->Light_Desc[1].Position = XMFLOAT3(-4.0f, 0.8f, -4.0f);
m_light->Light_Desc[1].Diffuse = XMFLOAT4(0.8f, 0.1f, 0.1f, 1.0f);
m_light->Light_Desc[2].Position = XMFLOAT3(-4.0f, 0.8f, 4.0f);
m_light->Light_Desc[2].Diffuse = XMFLOAT4(0.1f, 0.1f, 0.8f, 1.0f);
m_light->Light_Desc[3].Position = XMFLOAT3(4.0f, 0.8f, 4.0f);
m_light->Light_Desc[3].Diffuse = XMFLOAT4(0.1f, 0.9f, 0.1f, 1.0f);
}
#include"graphics.h"
graphicsclass::graphicsclass()
{
m_light = NULL;
}
graphicsclass::~graphicsclass()
{
}
BOOL graphicsclass::Init_Graphics()
{
Create_Mutil_Light(4);
m_light->Deploy_Shader(m_shader->GetIeffect());
return TRUE;
}
BOOL graphicsclass::Frame_Graphics()
{
m_light->Frame_Light();
}
void graphicsclass::Clean_Graphics()
{
m_light->Clean_Light();
delete m_light; m_light = NULL;
}
HLSL:就只有像素着色器需要改动:简单到令人心疼。
float4 PS(VertexOut In, uniform int lightCount) :SV_Target
{
float4 color = (float4)0;
float4 TexColor = (float4)0;
float3 normal = normalize(In.Normal);
float3 CameraPosition = float3(0.0f, 10.0f, -10.0f);
float3 ViewDir = normalize(CameraPosition - In.PosW);
float4 Ambient_Color = (float4)0;
float4 Diffuse_Color = (float4)0;
float4 Specular_Color = (float4)0;
float Mat_Specular = 0.1f;
TexColor = ShaderTexture.Sample(samTriLinear, In.Tex);
[unroll]
for (int i = 0; i < lightCount; i++)
{
ComputeSpotLight(Mat_Specular, g_SpotLight[i], In.PosW, normal, CameraPosition,
Ambient_Color, Diffuse_Color, Specular_Color);
Ambient_Color += float4(0.1f,0.1f,0.1f,1.0f);
color +=Ambient_Color + Diffuse_Color + Specular_Color;
}
color = TexColor*saturate(color);
return color;
}
ComputeSpotLight:来源于龙书11,其实只是把函数整合一下而已:
void ComputeSpotLight(float Specular, SpotLight L, float3 pos, float3
normal, float3 toEye,
out float4 ambient, out float4 diffuse, out float4
spec)
{
// 初始化输出变量.
ambient = float4(0.0f, 0.0f, 0.0f, 0.0f);
diffuse = float4(0.0f, 0.0f, 0.0f, 0.0f);
spec = float4(0.0f, 0.0f, 0.0f, 0.0f);
// 从表面指向光源的光照矢量
float3 lightVec = L.Position - pos;
// 表面离开光源的距离
float d = length(lightVec);
// Range test.
if (d > L.Range)
return;
// 规范化光照矢量
lightVec /= d;
// 计算环境光
ambient = L.Ambient;
// 计算漫反射和镜面光,provided the surface is in
// the line of site of the light.
float diffuseFactor = dot(lightVec, normal);
// Flatten 避免动态分支
[flatten]
if (diffuseFactor > 0.0f)
{
float3 v = reflect(-lightVec, normal);
float specFactor = pow(max(dot(v, toEye), 0.0f),Specular);
diffuse = diffuseFactor * L.Diffuse;
spec = specFactor* L.Specular;
}
// Scale by spotlight factor and attenuate.
float cos_spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
// Scale by spotlight factor and attenuate.
float att = cos_spot / dot(L.Att, float3(1.0f, d, d*d));
ambient *= cos_spot;
diffuse *= att;
spec *= att;
}
其实这里有个小纰漏,我们改变灯光的范围是通过控制余弦值的幂,上面有提及,这会是整体亮度降低,不符合我们将聚光灯收束的特性:聚光灯内部更亮,外围越暗,在数学意义上解释来说:y=cos(x)^n;当n=1时,从0到/2积分,面积为1,当n=2时,面积为/4,而我们的面积(光通量)应该是相等的,所以我们的提升光圈范围内的y值。
所以我们加了个小函数:
float cos_spot = pow(max(dot(-lightVec, L.Direction), 0.0f), L.Spot);
float Amend_spot = pow(abs((L.Spot+10) / L.Spot), L.Spot);
// Scale by spotlight factor and attenuate.
float att = cos_spot / dot(L.Att, float3(1.0f, d, d*d));
ambient *= cos_spot*Amend_spot;
diffuse *= att*Amend_spot;
spec *= att*Amend_spot;
当spot越大时,其函数值越小,对光通量的影响也就越小,以至于在spot比较大时,颜色计算可以忽略Amend_spot ,但无论怎么讲,此函数还是有效的的使光圈内的光通量增加,虽然不是按照正确的方式,这样即使是spot很大,我们仍然能让光圈内的亮度到达一个比较高的值。
spot=800时 对比图:
当然这个函数明显是错误的,因为spot=800时,光圈应该只有左边的大小,如果读者想实现真正的聚光灯,嗯恩,你数学得不错。
ok,总结到这基本上就要说byebye了,有一点提一下,如果读者使用了Amend_spot ,一定要把它放到cpu中去处理,这样太浪费了。
愿惠比寿老爷爷祝福我们。
源码链接:
https://pan.baidu.com/s/1NGt12oXXvmwYplv8tGoZAg