本系列文章主要翻译和参考自《Real-Time 3D Rendering with DirectX and HLSL》一书(感谢原书作者),同时会加上一点个人理解和拓展,文章中如有错误,欢迎指正。
这里是书中的代码和资源。
本文所有的环境和工具使用都基于之前的文章,如有不明白的地方请先参考本系列之前的几篇文章。
本文索引:
- 关于灯光类型
- Point Light 什么是点光源
- 1 Point Light Preamble 点光源变量准备
- 2 Point Light Vertex and Pixel Shader 点光源顶点及像素着色器的实现
- 3 Point Light Output 点光源输出效果
- 4 Point Light Modifications 修改点光源的实现
关于灯光类型
在前面的介绍光照模型的几篇文章中,主要为大家介绍了一些很基本的光照模型是如何实现的。文章中提到的计算方式都是在假定环境中只存在平行光的情况下计算的。之前我们有介绍过,灯光有很多种类型,包括点光源、平行光、聚光灯等……。下面将为大家介绍除平行光之外的这几种灯光。
Point Light: 什么是点光源
点光源的效果类似电灯泡,会向所有方向均匀地放出光,在场景中的点光源是由具体位置的区别的。这些就和平行光不一样,平行光没有具体位置的区别,他是从无限远的地方照射过来的光,并且是有一个统一的光线方向的。总的来说,平行光没有移动光源位置的区别,而点光源没有旋转光源的区别。
在模拟点光源的效果时,我们可以使用和平行光计算中一样的光照模型(包括漫反射光照模型和高光光照模型)。但是在点光源中,我们需要给出光源位置并计算光线方向。计算点光源的光线方向也很简单,就是取世界坐标系下的光源坐标减去世界坐标系下的物体坐标。下图表示了这个计算过程:
另外,由于点光源是有具体的位置的,你可以通过距离调节物体表面的光线强弱。点光源距离物体越远,物体表面越暗。下面的代码实现了当场景中只有一个点光源的时候的效果:
Listing 7.1 PointLight.fx(关于include文件Common.fxh如何引用请参考第八篇文章)
#include "include\\Common.fxh"
/*************** Resources ***************/
cbuffer CBufferPerFrame
{
float4 AmbientColor : AMBIENT
<
string UIName = "Ambient Light";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 0.0f};
float4 LightColor : COLOR
<
string Object = "LightColor0";
string UIName = "Light Color";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float3 LightPosition : POSITION
<
string Object = "PointLight0";
string UIName = "Light Position";
string Space = "World";
> = {0.0f, 0.0f, 0.0f};
float LightRadius
<
string UIName = "Light Radius";
string UIWidget = "slider";
float UIMin = 0.0;
float UIMax = 100.0;
float UIStep = 1.0;
> = {10.0f};
float3 CameraPosition : CAMERAPOSITION<string UIWidget="None";>;
}
cbuffer CBufferPerObject
{
float4x4 WorldViewProjection : WORLDVIEWPROJECTION <string UIWidget="None";>;
float4x4 World : WORLD <string UIWidget="None";>;
float4 SpecularColor : SPECULAR
<
string UIName = "Specular Color";
String UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float SpecularPower : SPECULARPOWER
<
string UIName = "Specular Power";
string UIWidget = "Slider";
float UIMin = 1.0;
float UIMax = 255.0;
float UIStep = 1.0;
> = {25.0f};
}
Texture2D ColorTexture
<
string ResourceName = "default_color.dds";
string UIName = "Color Texture";
string ResourceType = "2D";
>;
SamplerState ColorSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
RasterizerState DisableCulling
{
CullMode = NONE;
};
/*************** Data Structures ***************/
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
float3 Normal : NORMAL;
};
struct VS_OUTPUT
{
float4 Position : SV_Position;
float3 Normal : NORMAL;
float2 TextureCoordinate : TEXCOORD0;
float4 LightDirection : TEXCOORD1;
float3 ViewDirection : TEXCOORD2;
};
/*************** Vertex Shader ***************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
VS_OUTPUT OUT = (VS_OUTPUT)0;
OUT.Position = mul(IN.ObjectPosition, WorldViewProjection);
OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate);
OUT.Normal = normalize(mul(float4(IN.Normal, 0), World).xyz);
float3 worldPosition = mul(IN.ObjectPosition, World).xyz;
float3 lightDirection = LightPosition - worldPosition;
OUT.LightDirection.xyz = normalize(lightDirection);
OUT.LightDirection.w = saturate(1.0f -(length(lightDirection)/LightRadius));//Attenuation
OUT.ViewDirection = normalize(CameraPosition - worldPosition);
return OUT;
}
/*************** Pixel Shader ***************/
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 OUT = (float4)0;
float3 normal = normalize(IN.Normal);
float3 lightDirection = normalize(IN.LightDirection);
float3 viewDirection = normalize(IN.ViewDirection);
float n_dot_1 = dot(normal, lightDirection);
float3 halfVector = normalize(lightDirection + viewDirection);
float n_dot_h = dot(normal, halfVector);
float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
float4 lightCoefficients = lit(n_dot_1, n_dot_h, SpecularPower);
float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb);
float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb) * IN.LightDirection.w;
float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w)) * IN.LightDirection.w;
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
/*************** Techniques ***************/
technique10 main10
{
pass p0
{
SetVertexShader(CompileShader(vs_4_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, pixel_shader()));
SetRasterizerState(DisableCulling);
}
}float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w));
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
/*************** Techniques ***************/
technique10 main10
{
pass p0
{
SetVertexShader(CompileShader(vs_4_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, pixel_shader()));
SetRasterizerState(DisableCulling);
}
}
(1) Point Light Preamble : 点光源变量准备
这段代码中的大部分代码是从上一篇文章中的blinn-phong模型下的代码复制过来的,如有不明白的代码可以翻看之前的文章。
CBufferPerFrame代码块中的LightPosition表示点光源在场景中世界坐标系下的位置。LightRadius表示点光源的作用距离,超过这个距离,点光源将不会造成任何影响。
与之前相比,顶点着色器输出结构体中的LightDirection从float3变成了float4,多出来的第四个通道是用来存储光线的强弱的。也就是说光线的强弱和方向都是在顶点着色器中计算出来的。
注意 |
将光线强度保存在LightDirection中的w通道只是为了节省性能,在这个变量中,xyz足以表示光线方向,而w一般是不会再用到的了。因此,与其再为了这个数据多占用输出结构中的一个变量,不如直接将其保存在w中。但光线强弱和光线方向是没有任何关系的。 |
(2) Point Light Vertex and Pixel Shader : 点光源顶点及像素着色器的实现
顶点着色器中,计算出了光线方向并将其保存在LightDirection变量的xyz中。光线强度通过以下公式计算得出:
公式中的lightDirection是直接通过顶点坐标和光源位置计算出的,还没有单位化,因此可以通过除以lightRadius得出光线强弱比例。计算得出的attenuation通过saturate函数保证其结果不会小于0。
像素着色器中并不需要做特殊处理,直接将顶点着色器中计算出的光线强度加入到漫反射和高光的计算中。
(3) Point Light Output : 点光源输出效果
下图展示了一个只带有点光源的场景的显示效果,左图中的球体离点光源距离较近,右图中则较远。图中黄色线框所表示的就是一个点光源的位置:
(4) Point Light Modifications : 修改点光源的实现
从下面的左右两张图中可以发现高光点效果的不同,这是因为之前使用顶点着色器计算点光源的时候会产生的一些瑕疵。这种瑕疵只会出现在当点光源距离模型表面十分接近时,这个时候光向量在根据顶点计算时顶点间的向量差异会更大,而顶点间的这些像素会继续使用顶点上计算出的这些光向量数据。
代码段Listing 7.2 PointLight-Imp.fx
#include "include\\Common.fxh"
/*************** Resources ***************/
cbuffer CBufferPerFrame
{
float4 AmbientColor : AMBIENT
<
string UIName = "Ambient Light";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 0.0f};
float4 LightColor : COLOR
<
string Object = "LightColor0";
string UIName = "Light Color";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float3 LightPosition : POSITION
<
string Object = "PointLight0";
string UIName = "Light Position";
string Space = "World";
> = {0.0f, 0.0f, 0.0f};
float LightRadius
<
string UIName = "Light Radius";
string UIWidget = "slider";
float UIMin = 0.0;
float UIMax = 100.0;
float UIStep = 1.0;
> = {10.0f};
float3 CameraPosition : CAMERAPOSITION<string UIWidget="None";>;
}
cbuffer CBufferPerObject
{
float4x4 WorldViewProjection : WORLDVIEWPROJECTION <string UIWidget="None";>;
float4x4 World : WORLD <string UIWidget="None";>;
float4 SpecularColor : SPECULAR
<
string UIName = "Specular Color";
String UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
float SpecularPower : SPECULARPOWER
<
string UIName = "Specular Power";
string UIWidget = "Slider";
float UIMin = 1.0;
float UIMax = 255.0;
float UIStep = 1.0;
> = {25.0f};
}
Texture2D ColorTexture
<
string ResourceName = "default_color.dds";
string UIName = "Color Texture";
string ResourceType = "2D";
>;
SamplerState ColorSampler
{
Filter = MIN_MAG_MIP_LINEAR;
AddressU = WRAP;
AddressV = WRAP;
};
RasterizerState DisableCulling
{
CullMode = NONE;
};
/*************** Data Structures ***************/
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
float3 Normal : NORMAL;
};
struct VS_OUTPUT
{
float4 Position : SV_Position;
float3 Normal : NORMAL;
float2 TextureCoordinate : TEXCOORD0;
float3 WorldPosition : TEXCOORD1;
float3 Attenuation : TEXCOORD2;
};
/*************** Vertex Shader ***************/
VS_OUTPUT vertex_shader(VS_INPUT IN)
{
VS_OUTPUT OUT = (VS_OUTPUT)0;
OUT.Position = mul(IN.ObjectPosition, WorldViewProjection);
OUT.WorldPosition = mul(IN.ObjectPosition, World).xyz;
OUT.TextureCoordinate = get_corrected_texture_coordinate(IN.TextureCoordinate);
OUT.Normal = normalize(mul(float4(IN.Normal, 0), World).xyz);
float3 lightDirection = LightPosition - OUT.WorldPosition;
OUT.Attenuation = saturate(1.0f - (length(lightDirection)/LightRadius));
return OUT;
}
/*************** Pixel Shader ***************/
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 OUT = (float4)0;
float3 normal = normalize(IN.Normal);
float3 lightDirection = normalize(LightPosition - IN.WorldPosition);
float3 viewDirection = normalize(CameraPosition - IN.WorldPosition);
float n_dot_1 = dot(normal, lightDirection);
float3 halfVector = normalize(lightDirection + viewDirection);
float n_dot_h = dot(normal, halfVector);
float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
float4 lightCoefficients = lit(n_dot_1, n_dot_h, SpecularPower);
float3 ambient = get_vector_color_contribution(AmbientColor, color.rgb);
float3 diffuse = get_vector_color_contribution(LightColor, lightCoefficients.y * color.rgb) * IN.Attenuation;
float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w)) * IN.Attenuation;
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
/*************** Techniques ***************/
technique10 main10
{
pass p0
{
SetVertexShader(CompileShader(vs_4_0, vertex_shader()));
SetGeometryShader(NULL);
SetPixelShader(CompileShader(ps_4_0, pixel_shader()));
SetRasterizerState(DisableCulling);
}
}
在这段代码中,将光向量和视线向量移到像素着色器中计算,这样加大了计算量,但同时也是显示结果更为精细。但并没有将光线强度也加入到像素着色器中,因为当距离足够近时,两个顶点间的光线强度并不会差很多。虽然这样计算了两次光线方向,一次是在顶点着色器中为了计算光线强度的,一次是在像素着色器中计算每个像素点上的光线方向。但这样也比把所有计算都移到像素着色器中要更能节省一点性能。