GLSL逐顶点的光照

本文转自下述博文,并在此基础上做了微小改动:
1. 【GLSL教程】(六)逐顶点的光照
2. 【GLSL教程】(七)逐像素的光照


引言

在OpenGL中有三种类型的光:方向光(directional)、点光(point)、聚光(spotlight)。

逐顶点光照的方向光,向shader中逐渐添加环境光、散射光和高光效果。

GLSL逐顶点的光照_第1张图片
然后对方向光使用逐像素光照以获得更好的效果。
GLSL逐顶点的光照_第2张图片
最后实现逐像素的点光和聚光。这些内容与方向光很相近,大部分代码都是通用的。
GLSL逐顶点的光照_第3张图片
OpenGL通过固定管线向GLSL传递下述光源参数(osg中通过osg::Light类进行传递):

struct gl_LightSourceParameters  
{
    vec4 ambient;  
    vec4 diffuse;  
    vec4 specular;  
    vec4 position;  
    vec4 halfVector;  
    vec3 spotDirection;  
    float spotExponent;  
    float spotCutoff;     // (range: [0.0,90.0], 180.0)  
    float spotCosCutoff;  // (range: [1.0,0.0],-1.0)  
    float constantAttenuation;  
    float linearAttenuation;  
    float quadraticAttenuation;  
};  
uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];  //osg::Light类传递

struct gl_LightModelParameters  
{  
    vec4 ambient;  
};  
uniform gl_LightModelParameters gl_LightModel;   //osg::LightModel可以传递

在GLSL中也同样可以访问材质参数(osg通过osg::Material传递):

struct gl_MaterialParameters  
{  
    vec4 emission;  
    vec4 ambient;  
    vec4 diffuse;  
    vec4 specular;  
    float shininess;  
};  

uniform gl_MaterialParameters gl_FrontMaterial;  
uniform gl_MaterialParameters gl_BackMaterial; 

参数具体含义请按参考:GLSL内置变量详解


1.1 逐顶点方向光 —– Lambert模型(漫反射)

在OpenGL中假定,不管观察者的角度如何,得到的散射光强度总是相同的。散射光的强度与光源中散射光成分以及材质中散射光反射系数相关,此外也和入射光角度与物体表面法线的夹角相关。

GLSL逐顶点的光照_第4张图片
OpenGL用下面的公式计算散射光成分:
这里写图片描述
其中I是反射光的强度,Ld是光源的散射成分(gl_LightSource[0].diffuse),Md是材质的散射系数(gl_FrontMaterial.diffuse)。

这个公式就是Lambert漫反射模型。Lambert余弦定律描述了平面散射光的亮度,正比于平面法线与入射光线夹角的余弦,这一理论提出已经超过200年了。

在顶点shader中要实现这个公式,需要用到光源参数中的方向、散射成分强度,还要用到材质中的散射成分值。因此使用此shader时,在OpenGL中需要像在平时一样设置好光源。注意:由于没有使用固定功能流水线,所以不需要对光源调用glEnable。(对于OSG的话,按照灯光的正常使用方法进行

要计算余弦值,首先要确保光线方向向量(gl_LightSource[0].position)与法线向量都是归一化的,然后就可以使用点积得到余弦值。注意:对方向光,OpenGL中保存的方向是从顶点指向光源,与上面图中画的相反。

OpenGL将光源的方向保存在视点空间坐标系内,因此我们需要把法线也变换到视点空间。完成这个变换可以用预先定义的一致变量gl_NormalMatrix。这个矩阵是模型视图变换矩阵的左上3×3子矩阵的逆矩阵的转置。

//顶点着色器
void main()  
{  
    vec3 normal, lightDir;  
    vec4 diffuse;  
    float NdotL;  

    /* first transform the normal into eye space and normalize the result */  
    normal = normalize(gl_NormalMatrix * gl_Normal);  
    /* now normalize the light's direction. Note that according to the 
    OpenGL specification, the light is stored in eye space. Also since 
    we're talking about a directional light, the position field is actually 
    direction */  
    lightDir = normalize(vec3(gl_LightSource[0].position));  
    /* compute the cos of the angle between the normal and lights direction. 
    The light is directional so the direction is constant for every vertex. 
    Since these two are normalized the cosine is the dot product. We also 
    need to clamp the result to the [0,1] range. */  
    NdotL = max(dot(normal, lightDir), 0.0);  
    /* Compute the diffuse term */  
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;  
    gl_FrontColor =  NdotL * diffuse;  

    gl_Position = ftransform();  
}   
//片段着色器
void main()  
{  
    gl_FragColor = gl_Color;  
}  

下图显示了应用此shader的茶壶效果。注意茶壶的底部非常黑,这是因为还没有使用环境光的缘故。

GLSL逐顶点的光照_第5张图片
加入环境光非常容易,只需要使用一个全局的环境光参数以及光源的环境光参数即可,公式如下所示:
这里写图片描述
前面的顶点shader中需要加入几条语句完成环境光的计算:

//顶点着色器
void main()  
{  
    vec3 normal, lightDir;  
    vec4 diffuse, ambient, globalAmbient;  
    float NdotL;  

    normal = normalize(gl_NormalMatrix * gl_Normal);  
    lightDir = normalize(vec3(gl_LightSource[0].position));  
    NdotL = max(dot(normal, lightDir), 0.0);  
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;  
    /* Compute the ambient and globalAmbient terms */  
    ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;  
    globalAmbient = gl_FrontMaterial.ambient * gl_LightModel.ambient;  
    gl_FrontColor =  NdotL * diffuse + globalAmbient + ambient;  

    gl_Position = ftransform();  
}  

下图显示了最终效果。加入环境光后整个画面都变亮了,不过相对于应用了反射光效果的全局光照模型(global illumination model),这种计算环境光的方式只能算廉价的解决方案。

GLSL逐顶点的光照_第6张图片


1.2 逐顶点方向光 —– Blinn-Phong光照模型(修正镜面光)

我们使用称为Blin-Phong模型的光照模型,这是Phong模型的简化修正版本。在这之前,我们有必要先看看Phong模型,以便于更好地理解Blin-Phong模型。

在Phong模型中,镜面反射成分和反射光线与视线夹角的余弦相关,如下图:

GLSL逐顶点的光照_第7张图片
其中L表示入射光,N表示法线,Eye表示从顶点指向观察点的视线,R是L经镜面反射后的结果,镜面反射成分与α角的余弦相关。

如果视线正好和反射光重合,我们将接收到最大的反射强度。当视线与反射光相背离时,反射强度将随之下降,下降速率可以由一个称为shininess的因子控制,shininess的值越大,下降速率越快。也就是说,shininess越大的话,镜面反射产生的亮点就越小。在OpenGL中这个值的范围是0到128。

GLSL逐顶点的光照_第8张图片
计算反射光向量的公式:
11
OpenGL中使用Phong模型计算镜面反射成分的公式:
12
式中指数s就是shininess因子,Ls是光源中镜面反射强度,Ms是材质中的镜面反射系数。

Blinn提出了一种简化的模型,也就是Blinn-Phong模型。它基于半角向量(half-vector),也就是方向处在观察向量以及光线向量之间的一个向量:(注意上方光线反射示意图中向量L的方向!!!)

13
现在可以利用半角向量和法线之间夹角的余弦来计算镜面反射成分。OpenGL所使用的Blinn-Phong模型计算镜面反射的公式如下:
14
这个方法与显卡的固定流水线中使用的方法相同。因为我们要模拟OpenGL中的方向光,所以在shader中也使用此公式。幸运的是:OpenGL会帮我们算半角向量,我们只需要使用下面的代码:

/* compute the specular term if NdotL is  larger than zero */  
if (NdotL > 0.0)  
{  
    // normalize the half-vector, and then compute the cosine with the normal  
    NdotHV = max(dot(normal, gl_LightSource[0].halfVector.xyz), 0.0);  
    specular = gl_FrontMaterial.specular * gl_LightSource[0].specular * pow(NdotHV, gl_FrontMaterial.shininess);  
} 

1.3 逐像素方向光

现将前面的shader代码改为逐像素计算的方向光。首先看看每个顶点接收到的信息:

  • 法线
  • 半向量
  • 光源方向

我们需要将法线变换到视点空间然后归一化。我们还需要将半向量和光源方向也归一化,不过它们已经位于视点空间中了。这些归一化之后的向量会进行插值,然后送入片断shader,所以需要声明易变变量保存这些向量。也可以在顶点shader中完成一些与光和材质相关的计算,这样可以帮助平衡顶点shader和片断shader的负载。
顶点shader代码可以写成如下形式:

varying vec4 diffuse,ambient;  
varying vec3 normal,lightDir,halfVector;  

void main()  
{  
    /* first transform the normal into eye space and 
    normalize the result */  
    normal = normalize(gl_NormalMatrix * gl_Normal);  
    /* now normalize the light's direction. Note that 
    according to the OpenGL specification, the light 
    is stored in eye space. Also since we're talking about 
    a directional light, the position field is actually direction */  
    lightDir = normalize(vec3(gl_LightSource[0].position));  
    /* Normalize the halfVector to pass it to the fragment shader */  
    halfVector = normalize(gl_LightSource[0].halfVector.xyz);  
    /* Compute the diffuse, ambient and globalAmbient terms */  
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;  
    ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;  
    ambient += gl_FrontMaterial.ambient * gl_LightModel.ambient;  

    gl_Position = ftransform();  
}  

接下来在片断shader中,首先要声明同样的易变变量。此外还要再次对法线进行归一化,光线向量不需要进行归一化了,因为方向光对所有顶点都是一致的,插值得到的结果自然也不会变。之后就是计算插值过的法线向量与光线向量的点积。

varying vec4 diffuse,ambient;  
varying vec3 normal,lightDir,halfVector;  

void main()  
{  
    vec3 n,halfV;  
    float NdotL,NdotHV;  
    /* The ambient term will always be present */  
    vec4 color = ambient;  
    /* a fragment shader can't write a varying variable, hence we need 
    a new variable to store the normalized interpolated normal */  
    n = normalize(normal);  
    /* compute the dot product between normal and ldir */  
    NdotL = max(dot(n,lightDir),0.0);  
    ...  

如果点积结果NdotL大于0,我们就必须计算散射光,也就是用顶点shader传过来的散射项乘以这个点积。我们还需要计算镜面反射光,计算时首先对接收到的半向量归一化,然后计算半向量和法线之间的点积。

...  
if (NdotL > 0.0)  
{  
    color += diffuse * NdotL;  
    halfV = normalize(halfVector);  
    NdotHV = max(dot(n,halfV),0.0);  
    color += gl_FrontMaterial.specular *  
            gl_LightSource[0].specular *  
            pow(NdotHV, gl_FrontMaterial.shininess);  
}  

gl_FragColor = color;  

下图显示了逐像素光照和逐顶点光照效果的区别:

GLSL逐顶点的光照_第9张图片


2.1 逐像素点光(Point Light Per Pixel)

本节基于前面有关方向光的内容,大部分代码都相同。本节内容主要涉及方向光和点光的不同之处。方向光一般假设光源在无限远的地方,所以到达物体时是平行光。相反,点光源有一个空间中的位置,并向四面八方辐射光线。此外,点光的强度会随到达顶点的距离而衰弱。

对于OpenGL程序来说,这两种光的区别主要有:

  • 光源position域的w分量:对方向光来说它是0,表示这个position是一个方向(direction);对点光来说,这个分量是1,代表着它是一个w为1的齐次坐标。
  • 点光源的衰减由三个系数决定:一个常数项,一个线性项和一个二次项。

对方向光来说,光线的方向对所有顶点相同,但是对点光来说,方向是从顶点指向光源位置的向量。因此对我们来说需要修改的就是在顶点shader中加入计算光线方向的代码。

在OpenGL中衰减是按照如下公式计算的:

16
式中k0是常数衰减系数,k1是线性衰减系数,k2是二次衰减系数,d是光源位置到顶点的距离。

注意衰减与距离是非线性关系,所以我们不能逐顶点计算衰减再在片断shader中使用插值结果(所以使用的是逐像素点光,而非逐顶点点光),不过我们可以在顶点shader中计算距离,然后在片断shader中使用距离的插值计算衰减。

使用点光计算颜色值的公式为:

17
在上面公式中,环境光部分必须分解为两项:使用光照模型的全局环境光设置和光源中的环境光设置。顶点shader也必须分别计算这两个环境光成分。新的顶点shader如下:

varying vec4 diffuse,ambientGlobal,ambient;  
varying vec3 normal,lightDir,halfVector;  
varying float dist;  

void main()  
{  
    vec4 ecPos;  
    vec3 aux;  
    normal = normalize(gl_NormalMatrix * gl_Normal);  

    /* these are the new lines of code to compute the light's direction */  
    ecPos = gl_ModelViewMatrix * gl_Vertex;  
    aux = vec3(gl_LightSource[0].position-ecPos);  
    lightDir = normalize(aux);  
    dist = length(aux);  
    halfVector = normalize(gl_LightSource[0].halfVector.xyz);  

    /* Compute the diffuse, ambient and globalAmbient terms */  
    diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse;  
    /* The ambient terms have been separated since one of them */  
    /* suffers attenuation */  
    ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;  
    ambientGlobal = gl_FrontMaterial.ambient * gl_LightModel.ambient;  
    gl_Position = ftransform();  
}  

在片断shader中需要计算衰减,需要将插值得到的光线方向向量归一化,因为一般来说照到每个顶点的光线方向都不同。

varying vec4 diffuse,ambientGlobal, ambient;  
varying vec3 normal,lightDir,halfVector;  
varying float dist;  

void main()  
{  
    vec3 n,halfV,viewV,ldir;  
    float NdotL,NdotHV;  
    vec4 color = ambientGlobal;  
    float att;  
    /* a fragment shader can't write a varying variable, hence we need 
    a new variable to store the normalized interpolated normal */  
    n = normalize(normal);  
    /* compute the dot product between normal and normalized lightdir */  
    NdotL = max(dot(n,normalize(lightDir)),0.0);  
    if (NdotL > 0.0)  
    {  
        att = 1.0 / (gl_LightSource[0].constantAttenuation +  
                gl_LightSource[0].linearAttenuation * dist +  
                gl_LightSource[0].quadraticAttenuation * dist * dist);  
        color += att * (diffuse * NdotL + ambient);  
        halfV = normalize(halfVector);  
        NdotHV = max(dot(n,halfV),0.0);  
        color += att * gl_FrontMaterial.specular * gl_LightSource[0].specular *  
                        pow(NdotHV,gl_FrontMaterial.shininess);  
    }  
    gl_FragColor = color;  
}  

下图显示了固定功能的逐顶点与本节中逐像素计算得到的点光效果:

GLSL逐顶点的光照_第10张图片


3.1 逐像素的聚光(Spot Light Per Pixel)

本节内容与上一节基本一致,唯一不同的就是聚光不同于点光,其发出的光线被限制在一个圆锥体中。

对于OpenGL程序来说,这两种光的区别主要有:

  • 聚光包含一个方向向量spotDirection,表示圆锥体的轴。
  • 圆锥体包含一个角度,在GLSL中可以使用应用程序设置的角度值以及对应的余弦值spotCosCutoff。
  • 最后还有一个衰减速率spotExponent,它表示从圆锥的中心轴向外表面变化时光强度的衰减。

聚光的顶点shader与点光完全相同,我们只需要对片断shader进行一些修改。只有当当前片断位于聚光的光锥内时,才需要对散射光、镜面反射光和环境光成分进行着色,所以我们首先要检查这个条件。即当光源与某点连线向量与聚光方向向量(spotDirection)之间夹角的余弦值必须大于spotCosCutoff,否则此点位于聚光之外,只能接收到全局环境光。

...  

n = normalize(normal);  

/* compute the dot product between normal and ldir */  
NdotL = max(dot(n,normalize(lightDir)),0.0);  

if (NdotL > 0.0)  
{  
    spotEffect = dot(normalize(gl_LightSource[0].spotDirection),  
            normalize(-lightDir));  
    if (spotEffect > gl_LightSource[0].spotCosCutoff)  
    {  
        /* compute the illumination in here */  
    }  
}  

gl_FragColor = ...  

下面的光照计算与点光非常相似,唯一区别是衰减必须乘以聚光效果(spotlight effect),这个值按如下公式计算:

19
上式中spotDirection来自OpenGL中设置的状态,lightDir是光源到某点的向量,spotExp是聚光衰减率,这个值也是在OpenGL程序中设置的,它用来控制从聚光光锥中心到边缘的衰减。spotExp越大衰减越快,如果为0表示在光锥内光强是常数。

spotEffect = pow(spotEffect, gl_LightSource[0].spotExponent);  
att = spotEffect / (gl_LightSource[0].constantAttenuation +  
        gl_LightSource[0].linearAttenuation * dist +  
        gl_LightSource[0].quadraticAttenuation * dist * dist);  

color += att * (diffuse * NdotL + ambient);  

halfV = normalize(halfVector);  
NdotHV = max(dot(n,halfV),0.0);  
color += att * gl_FrontMaterial.specular * gl_LightSource[0].specular *  
               pow(NdotHV,gl_FrontMaterial.shininess);  

下图分别显示了使用固定功能流水线的逐顶点光照计算,以及使用本节shader的逐像素光照计算得到的聚光效果。

GLSL逐顶点的光照_第11张图片

你可能感兴趣的:(OpenGL)