摘抄“GPU Programming And Cg Language Primer 1rd Edition” 中文名“GPU编程与CG语言之阳春白雪下里巴人”
Lambert 模型较好地表现了粗糙表面上的光照现象,如石灰粉刷的墙壁、纸张等,但在用于诸如金属材质制成的物体时,则会显得呆板,表现不出光泽,主要原因是该模型没有考虑这些表面的镜面反射效果。一个光滑物体被光照射时,可以在某个方向上看到很强的反射光,这是因为在接近镜面反射角的一个区域内,反射了入射光的全部或绝大部分光强,该现象称为镜面反射。
故此, Phong Bui Tuong 提出一个计算镜面反射光强的经验模型,称为phong 模型,认为镜面反射的光强与反射光线和视线的夹角相关,其数学表达如公式(9-5) 所示:
Ks为材质的镜面反射系数, Ns 是高光指数, V 表示从顶点到视点的观察方向, R 代表反射光方向。
高光指数反映了物体表面的光泽程度。 Ns 越大,反射光越集中,当偏离反射方向时,光线衰减的越厉害,只有当视线方向与反射光线方向非常接近时才能看到镜面反射的高光现象,此时,镜面反射光将会在反射方向附近形成亮且小的光斑; Ns 越小,表示物体越粗糙,反射光分散,观察到的光斑区域小,强度弱。
反射光的方向 R 可以通过入射光方向 L (从顶点指向光源)和物体法向量 N 求出:
所以有:
实际上, Cg 语言标准函数库中有求取反射光方向的函数(参阅 8.3.2 节),不过掌握求取反射光方向的方法是有必要的,很多公司在考计算机图形学算法时会要求你写出这个公式,到时候诸如 “ 我知道某某函数可以实现这个功能 ” 之类的话语是不会打动面试官的。自从微软经常拿 VS 中的一些函数要求面试者写出实现时,这种笔试方法就形成了一种潮流,不过如果有可能的话,我很想对这种笔试方法说: “ 滚 ” !
Phong 光照模型的渲染代码如 代码 3 所示。 图 18 展示了在顶点着色程序中进行 phong 光照渲染的效果:
从 图 18 可以看出,与漫反射模型的渲染效果相比, phong 光照模型的渲染效果要圆润很多,明暗界限分明,光斑效果突出。不过请注意 图 18 中马的渲染效果,可以很清楚的发现,马的渲染效果没有其他三个模型好,原因在于马模型的面片少,是低精度模型,而顶点着色渲染只对几何顶点进行光照处理,并不会对内部点进行处理。
为了使得低精度模型也能得到高质量的渲染效果,就必须进行片段渲染,所以本节中我们还将给出使用片段着色程序的 phong 光照模型渲染代码和效果。
代码 3 phong 光照模型的顶点着色程序实现
struct VertexIn
{
float4 position : POSITION; // Vertex in object-space
float4 normal : NORMAL;
};
struct VertexScreen
{
float4 oPosition : POSITION;
float4 color : COLOR;
};
void main_v( VertexIn posIn,
out VertexScreen posOut,
uniform float4x4 modelViewProj,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT,
uniform float3 globalAmbient,
uniform float3 eyePosition,
uniform float3 lightPosition,
uniform float3 lightColor,
uniform float3 Kd,
uniform float3 Ks,
uniform float shininess)
{
posOut.oPosition = mul(modelViewProj, posIn.position);
float3 worldPos = mul(worldMatrix, posIn.position).xyz;
float3 N = mul(worldMatrix_IT, posIn.normal).xyz;
N = normalize(N);
// 计算入射光方向、视线方向、反射光线方向
float3 L = normalize(lightPosition - worldPos);
float3 V = normalize(eyePosition - worldPos);
float3 R = 2*max(dot(N, L), 0)*N-L;
R = normalize(R);
// 计算漫反射分量
float3 diffuseColor = Kd * globalAmbient+Kd*lightColor*max(dot(N, L), 0);
// 计算镜面反射分量
float3 specularColor = Ks * lightColor*pow(max(dot(V, R), 0), shininess);
posOut.color.xyz = diffuseColor + specularColor;
posOut.color.w = 1;
}
下面给出同时使用顶点着色程序和片段着色程序的 phong 光照模型代码。依然是首先定义结构体,用来包含输入、输出数据流,不过这里使用的结构体( 代码 4 )和 代码 3 中的有所不同,在 VertexScreen 结构体中有两个绑定到 TEXCOORD 语义词的变量, objectPos 和 objectNormal ,这两个变量用于传递顶点模型空间坐标和法向量坐标到片段着色器中。
代码 4 phong 光照模型片段着色实现的结构体
struct VertexIn
{
float4 position : POSITION;
float4 normal : NORMAL;
};
struct VertexScreen
{
float4 oPosition : POSITION;
float4 objectPos : TEXCOORD0;
float4 objectNormal : TEXCOORD1;
};
代码 5 展示了当前的顶点着色程序代码,其所做的工作有两点:首先将几何顶点的模型空间坐标转换为用于光栅化的投影坐标;然后将顶点模型坐标和法向量模型坐标赋值给绑定 TEXCOORD 语义词的变量,用于传递到片段着色程序中。
代码 5 phong 光照模型顶点着色程序
void main_v(VertexIn posIn,
out VertexScreen posOut,
uniform float4x4 modelViewProj)
{
posOut.oPosition = mul(modelViewProj, posIn.position);
posOut.objectPos = posIn.position;
posOut.objectNormal = posIn.normal;
}
代码 6 展示了使用 phong 光照模型渲染的片段着色程序。我将反射光方向、视线方向、入射光方向都放在片段着色程序中计算,实际上这些光照信息也可以放到顶点着色程序中计算,然后传递到片段着色程序中。
代码 6 phong 光照模型片段着色程序
void main_f( VertexScreen posIn,
out float4 color : COLOR,
uniform float4x4 worldMatrix,
uniform float4x4 worldMatrix_IT,
uniform float3 globalAmbient,
uniform float3 eyePosition,
uniform float3 lightPosition,
uniform float3 lightColor,
uniform float3 Kd,
uniform float3 Ks,
uniform float shininess)
{
float3 worldPos = mul(worldMatrix, posIn.objectPos).xyz;
float3 N = mul(worldMatrix_IT, posIn.objectNormal).xyz;
N = normalize(N);
// 计算入射光方向、视线方向、反射光线方向
float3 L = normalize(lightPosition - worldPos);
float3 V = normalize(eyePosition - worldPos);
float3 R = 2*max(dot(N, L), 0)*N-L;
R = normalize(R);
// 计算漫反射分量
float3 diffuseColor = Kd * globalAmbient+Kd*lightColor*max(dot(N, L), 0);
// 计算镜面反射分量
float3 specularColor = Ks * lightColor*pow(max(dot(V, R), 0), shininess);
color.xyz = diffuseColor + specularColor;
color.w = 1;
}
图 19 展示了同时使用顶点着色程序和片段着色程序的 phong 光照模型渲染效果。