本系列文章主要翻译和参考自《Real-Time 3D Rendering with DirectX and HLSL》一书(感谢原书作者),同时会加上一点个人理解和拓展,文章中如有错误,欢迎指正。
这里是书中的代码和资源。
本文所有的环境和工具使用都基于之前的文章,如有不明白的地方请先参考本系列之前的几篇文章。
本文索引:
在现实世界中,没有光我们将看不见任何东西,你所看见的物体或者是反射了光源的光或者是本身就能自发光。在计算机渲染的过程中,你将模拟灯光与物体的交互,并以此增加3D物体表面的细节。但是灯光的相互影响是一个非常复杂的过程,在目前的技术中并不能达到在一个可交互的帧率范围内进行这样大量的重复计算。因此,一般会采用一种近似算法,用一种描述灯光与3D模型如何交互的灯光模型来为你的感官增加更多可感受的细节。这篇文章中将介绍最基础的光照模型——环境光的计算模型。
环境光是看起来在明亮环境中无所不在的光。比如说,看下你的桌底,尽管没有光源直接照射,你似乎还是可以看到桌下的深远地方并且描绘出其外形。这种光是通过物体表面与光线的不断交互产生的(比如不断反射、折射叠加产生的)。当光线到达物体表面时,他们会被部分或完全的反射、吸收掉,这个过程会持续无数次。所以还是会有光线可以到底你的桌底,即使只是很小的一部分。
你可以通过设置一个数值来调整每个像素的颜色,以此来模拟一个最简单的环境光。你可以将这个数值看成是明亮度或光线强度过滤,这个数值通常在[0, 1]范围内并通过与每个像素的颜色值相乘达到效果(相当于color中的alpha通道)。如果环境光强度接近0,物体看起来就会更黑。另外,你还可以增加颜色到环境光模型中,来模拟一个不是纯白的光源。这就需要对像素中的每个通道进行调整。下面的代码段是环境光效果的代码实现。
代码段Listing 6.1 AmbientLighting.fx
#define FLIP_TEXTURE_Y 1
cbuffer CBufferPerFrame
{
float4 AmbientColor : AMBIENT<
string UIName = "Ambient Light";
string UIWidget = "Color";
> = {1.0f, 1.0f, 1.0f, 1.0f};
}
cbuffer CBufferPerObject
{
float4x4 WorldViewProjection : WORLDVIEWPROJECTION<
string UIWidget="None";
>;
}
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;
};
/********************* struct *********************/
struct VS_INPUT
{
float4 ObjectPosition : POSITION;
float2 TextureCoordinate : TEXCOORD;
};
struct VS_OUTPUT
{
float4 Position : SV_Position;
float2 TextureCoordinate : TEXCOORD;
};
/********************* Utility function *********************/
float2 get_corrected_texture_coordinate(float2 textureCoordinate)
{
#if FLIP_TEXTURE_Y
return float2(textureCoordinate.x, 1.0 - textureCoordinate.y);
#else
return textureCoordinate;
#endif
}
/********************* 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);
return OUT;
}
/********************* Pixel Shader *********************/
float4 pixel_shader(VS_OUTPUT IN) : SV_Target
{
float4 OUT = (float4)0;
OUT = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
OUT.rgb *= AmbientColor.rgb * AmbientColor.a;//Color(.rgb) * intensity(.a)
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);
}
}
上面的这段代码和上一篇文章中的代码非常类似,但加入了一些基础环境光的内容。
首先,我们在这段代码中加入了AmbientColor这个着色器常量,这是一个float4类型的常量,代表了环境光的颜色和强度。光源的颜色保存在该常量的RGB通道中,强度保存在常量的Alpha通道中。注意,这个量被包含在了一个新的名为CBufferPerFrame的cbuffer声明中。在本系列的第五篇文章 Hello,Shaders!中提到过,cbuffer是用来根据所包含数据的更新频率组织数据常量的。在这个例子中AmbientColor的数值会被多个物体共享,并且需要在每一帧更新数据。这不同于包含在CBufferPerObject中的WorldViewProjection对象,每个使用AmbientLighting效果的对象这个值都不同。
其次,注意关联到AmbientColor常量的AMBIENT注解字符串。和其他所有的shader常量一样,这个注解字符串是可选的,但把这个注解字符串加入并不是什么坏事,用注解将常量联系起来后CPU可以通过语义获得常量值而不需要通过一个硬编码的常量名。
最后,AmbientColor常量在声明时已经对其赋予的初始化值{1.0f, 1.0f, 1.0f, 1.0f}。这会产生一个纯白色,并且带有全光照强度的环境光,这样的设置并不会改变你场景中物体的输出颜色。代码其余部分都和上一篇文章中的相同,除了像素着色器外。
像素着色器中对纹理进行颜色采样然后调整其输出。准确的来说像素着色器中通过采样器获得像素的颜色值后和AmbientColor.rgb * AmbientColor.a相乘。AmbientColor中的rgb将分别和a相乘最后得到一个三维向量值。所以环境光首先和光强度相乘得到一个调整的带有光强度和颜色和结果,这个结果在和从采样器中取得的像素颜色值相乘。
下图展示了一个带有环境光影响的shader的渲染效果。左图是一个纯白色颜色值为(1, 1, 1)带有0.5光强度的球体,右图是一个纯红色颜色值为(1, 0, 0)带有全光强度的球体。
你可以在FX Composer中改变环境光的颜色来查看不同效果,如下图所示:
注意,你可以调节环境光的颜色是因为之前在shader代码中关联了UIName和UIWidget注解,这是FX Composer提供的功能,如果你不在这个软件中的话这些注解可能是没用的,需要你自己去实现对应功能。
注意 |
一般来说,不要将从常量缓冲区中获得的数据再在shader中进行数学运算,而应该在CPU端做好这种运算再传输过来。例如,这段代码中ambientColor*ambient这个运算应该在CPU端就已经做好。在这里是由于FX Compose这个软件只提供了这样的接口来提供常量传输,如果是自己写的接口,例如用DirectX API就不需要这样。 |
这篇文章中讲解了最基础的环境光模型,他的计算方式很简单,但这个模型是所有光照模型的基础,不管后面介绍多少种光照模型,很多都需要和环境光模型相结合。