本系列文章主要翻译和参考自《Real-Time 3D Rendering with DirectX and HLSL》一书(感谢原书作者),同时会加上一点个人理解和拓展,文章中如有错误,欢迎指正。
这里是书中的代码和资源。
本文所有的环境和工具使用都基于之前的文章,如有不明白的地方请先参考本系列之前的几篇文章。
本文索引:
在现实世界中,没有光我们将看不见任何东西,你所看见的物体或者是反射了光源的光或者是本身就能自发光。在计算机渲染的过程中,你将模拟灯光与物体的交互,并以此增加3D物体表面的细节。但是灯光的相互影响是一个非常复杂的过程,在目前的技术中并不能达到在一个可交互的帧率范围内进行这样大量的重复计算。因此,一般会采用一种近似算法,用一种描述灯光与3D模型如何交互的灯光模型来为你的感官增加更多可感受的细节。这篇文章中将介绍一些基础的光照模型。
当你模拟漫反射时,你只要能够提供出粗糙、亚光的表面感就可以了。这对大部分情况都是适用的,并且这提供了高光其他部分的基础照明。但有些时候你也需要制作一个闪光的表面模拟,例如,抛光金属和大理石地板。这部分要讲解的就是如何去实现这种高光效果。
有很多种方法可以用来模拟高光反射。我们第一个要研究的高光模型就是冯氏反射模型,这个模型以其发明者的名字名字,就是来自犹他大学(University of Utah)的Bui Tuong Phong。
跟漫反射不同,高光的计算需要根据观察者(相机)的位置和角度来计算。你可以在现实世界中观察到,当你改变观察高光物体的位置和角度时,其光斑位置也会发生变化。冯氏模型阐述了高光的计算依赖于观察者的观察方向和反射光向量之间的夹角。公式如下:
这里的R是反射光向量,V是观察方向,而指数S是高光的大小。越小的高光其指数值越大。反射光用下面的公式计算:
设顶点的单位法向量为N,有公式:
R + L = (2N • L)N (这里再次提醒一下,L的方向是由顶点指向光源的)
由这个可以推出:
该公式中N代表表面法线,L代表光线向量。
下面的代码实现了一个冯氏模型(关于include文件Common.fxh如何引用请参考上一篇文章):
代码段Listing 6.5 Phong.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 LightDirection : DIRECTION
<
string Object = "DirectionalLight0";
string UIName = "Light Direction";
string Space = "World";
> = {0.0f, -1.0f, -1.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 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);
OUT.LightDirection = normalize(-LightDirection);
float3 worldPosition = mul(IN.ObjectPosition, World).xyz;
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(lightDirection, normal);
float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
float3 ambient = AmbientColor.rgb * AmbientColor.a * color.rgb;
float3 diffuse = (float3)0;
float3 specular = (float3)0;
if(n_dot_1 > 0)
{
diffuse = LightColor.rgb * LightColor.a * saturate(n_dot_1) * color.rgb;
//R = 2 * (N.L) * N - L
float3 reflectionVector = normalize(2 * n_dot_1 * normal - lightDirection);
//specular = R.V^n with gloss map in color texture's alpha channel
specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(reflectionVector, viewDirection)), SpecularPower), color.w);
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
OUT.rgb = ambient + diffuse;
OUT.a = color.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);
}
}
对比上一篇文章中的漫反射效果,CBufferPerFrame缓冲区中只增加了一个成员:CameraPosition。这个常量储存了摄像机的位置也间接决定了视线方向。当你为这个常量关联了CAMERAPOSITION语义时,NVIDIA FX Composer会自动将Render面板中的摄像机位置绑定到这个常量上。
CBufferPerObject缓冲区中增加了两个成员SpecularColor和SpecularPower。SpecularColor和环境光、平行光有同样的函数体;他定义了高光的颜色和强度。这个多出来的部分可以让你独立调整高光而不用依赖于平行光。SpecularPower是指冯氏高光模型中的指数部分,用来调整高光的强度。
VS_OUTPUT结构体中也多出一个ViewDirection向量,这个向量用来将计算好的视线方向传递给光栅阶段进行处理。
在顶点着色器中,计算出了视线方向。但计算之前必须先统一坐标系,因此需要借助world矩阵将其转换到世界坐标系下。
像素着色器中新加入了对高光部分的计算。这部分光只有在光线面向模型表面的时候才会有,所以必须先判断n_dot_1>0时才执行。
此外,像素着色器中比较复杂的是计算高光的那段代码,先将这段代码列在下面:
代码段Listing 6.6 Phong.fx中计算高光的代码
//specular = R.V^n with gloss map in color texture's alpha channel
specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(reflectionVector, viewDirection)), SpecularPower), color.w);
这段代码是根据冯氏光照模型的公式编写的。使用pow函数,其底数为反射光向量和视线向量的点乘,指数部分为SpeculiarPower常量。saturate()函数限制了计算结果在0.0到1.0之间,也就限制了角度必须在0~90度之间。
注释中也说明了高光的计算还依赖于光泽贴图(gloss map),这个贴图储存在纹理贴图的alpha通道中。光泽贴图,或者说高光贴图(specular map)要么存储在一张单独的纹理中(这张纹理只有一个通道),要么是像本文中的例子,存在材质贴图的某一部分,例如alpha通道中。高光贴图根据贴图制作者的意图改变了高光的计算结果。本文所使用的地球表面纹理中,海洋部分由于是水面应该是有高光反射的,而陆地部分应该是没有高光反射的。下面这张图展示了高光贴图的内容,可以看出陆地部分是黑色的,其值为0,这部分像素最后计算出来的高光部分的值应该也是0,因此只保留了漫反射部分的光照,而水面部分则会加入完全的高光效果。
下面的图片中,左图展示了带有高光通道的贴图的渲染效果,右图的纹理中则不带有高光通道,注意观察他们在陆地部分的反射光效果区别:
1977年,Jim Blinn提出了简化版的冯氏模型,基于Half-Vector对光照模型的计算进行修改,Half-Vector是入射光与视线向量的和向量的一半(其实也等于入射光向量与视线向量的中间向量,因为在计算Half-Vector前要对这两个向量先进行单位化),计算公式如下:
从该公式可以看出,计算出的H(Half-Vector)是视线向量和入射光线向量想加后的单位向量。Blinn-Phong光照模型由表面法线向量和Half-Vector计算出(与Phong模型的反射光和视线向量不同),这样增大了公式中的底数,计算公式如下:
由于Blinn-Phong光照模型是在Phong光照模型的基础上修改的,他们大部分代码都是相似的,因此,只在此列出不同的部分,以下是像素着色器代码:
代码段 Listing 6.7 BlinnPhong.fx中的像素着色器
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(lightDirection, normal);
float4 color = ColorTexture.Sample(ColorSampler, IN.TextureCoordinate);
float3 ambient = AmbientColor.rgb * AmbientColor.a * color.rgb;
float3 diffuse = (float3)0;
float3 specular = (float3)0;
if(n_dot_1 > 0)
{
diffuse = LightColor.rgb * LightColor.a * saturate(n_dot_1) * color.rgb;
float3 halfVector = normalize(lightDirection + viewDirection);
//specular = N.H^s with gloss map in color texture's alpha channel
specular = SpecularColor.rgb * SpecularColor.a * min(pow(saturate(dot(normal, halfVector)), SpecularPower), color.w);
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
Blinn-Phong光照模型和Phong光照模型的显示效果几乎是一样的,但由于他的底数加大了,所以需要调整指数,也就是高光强度部分才能和Phong光照模型效果一样。如下图所示,左图为Blinn-Phong光照模型渲染效果,右图为Phong光照模型渲染效果,两个material的光照强度值设置的都为25:
可以看出Blinn-Phong光照模型的光斑在同样的光照强度参数下明显要比Phong光照模型的大些。
关于内联函数(Intrinsics)的解释清查阅参考资料【1】中的instrinsics部分。
关于Intrinsics函数的原文引用说明 |
内联函数有可能会被误会成我们通常想的那样,主要是这个单词我翻译不正确(intrinsics).这样的函数可以被C和C++的程序所调用.看上去和别的函数没有很多的区别,最多也就名字比较古怪.但是其实当这个代码在被编译器编译的时候,它会被转化为有序的低级指令.这些指令就是NEON的指令了.所以这样就办到了在高级语言层次使用低级语言了.主要是很简单的可以使用.最为主要的就是程序员不用去接触汇编了,可以减小优化的难度.当然我可以说这样的优化效率没有使用汇编的来的高. |
对于上面的这种技术其实就是ARM公司本身给你做好了一些函数,你就直接调用这些函数,这些函数在编译的时候就可以直接转化成NEON的汇编指令.为了支持这些内联的函数所以必须要包含头文件arm_neon.h. |
文章中重要提到的是C里面的Intrinsics函数,但HLSL中的Intrinsics函数原理和他应该是类似的。
HLSL中提供了lit()函数,用来帮助计算Lambertian模型的漫反射部分和Blinn-Phong模型的高光部分。一个提升性能的很好的办法就是尽量在能使用Intrinsics函数或者说内置函数的时候使用它们,因为如上文所说这类型的函数在硬件中进行过优化。因此,我们将用lit()函数重写过的Blinn-Phong模型像素着色器列在下面:
代码段Listing 6.8 重写的Blinn-Phong光照模型像素着色器
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);
float3 specular = get_scalar_color_contribution(SpecularColor, min(lightCoefficients.z, color.w));
OUT.rgb = ambient + diffuse + specular;
OUT.a = 1.0f;
return OUT;
}
以上代码的改写和像素着色器有很多不同,为计算每种灯光单独封装了函数,所封装的get_vector_color_contribution和get_scalar_color_contribution函数在第七篇文章中提到的Common.fxh文件中已经写好了。并且在这个着色器中移除了条件判断句。新增加了lit()的调用,这个函数接受将n_dot_1和n_dot_h、高光强度作为输入参数,能实现和之前计算漫反射和blinn-phong高光模型计算出的结果一样的效果,返回值为float4,x和w的输出值总是1,y值代表的是这个像素上漫反射光照的计算结果,z值代表这个像素上blinn-phong光照计算结果。
Blinn-Phong 和 Phong两种光照模型的实现效果相似,只是需要调节高光强度的大小不一样,还有应该就是渲染效率的问题。由于Blinn-Phong 中涉及到平方根的运算(这个运算来源于对half-vector向量的normalize计算)。但是,之后我们使用了lit()函数,这个内联函数使得Blinn-Phong 比Phong模型的计算又稍微提升了一些,并且减少了代码量。因此,在后面需要使用到带有漫反射和高光的模型时都将使用这个函数。
这篇文章中主要讲解了两种高光模型的算法和shader实现。需要注意的是,本文中主要介绍了统一模型上混合了高光和漫反射的情况,但现实中也有很多模型是单一的,即比如玻璃杯和紫砂壶这类的物品他们的表面材质大部分是统一的。这种情况比较简单,只要注意对高光贴图的使用即可。
【1】内联函数。(http://www.360doc.com/content/14/0428/15/9408846_372927783.shtml)