要实现的是使用GLSL来模拟从太空角度观测的地球,该例子取自GLSL橙色书第10章。这个例子写的还是蛮有教学作用的,不过感觉把太多的重点放到了gloss map上面,这不要讲的是多重纹理来着吗。。。总之多学学没错。
一、要想实现对地球的模拟,我们可以画一个球,然后直接使用一副从太空拍摄的地球纹理贴图作于球面上(也就是简单的纹理映射),但是这样做会有一些“不真实”的地方:
(1)我们知道地球上是有一些人为manmade的光源(比如各大城市所发出的光芒),当夜晚来临的时候,即使从太空中看地球,各大城市也会发出明显的光芒。所以我们得判断地表上的每一处(其实就是球面上的每一个vertex)是白天还是黑夜,这个可以通过灯光向量和球面顶点法向量的夹角值来判断。当地表某点处于白天时,我们使用一副daytime的纹理位图并作一些相关的光照计算,而当处于夜晚时,我们不进行光照融合计算而是直接使用一副黑夜纹理贴图。见下图,分别是白天/黑夜纹理:
图1.1 daytime纹理贴图
图1.2 nighttime纹理贴图(感觉灯光的亮度表现了地域的繁荣度)
(2)另一个不真实的地方在于,地球上的海水具有较强的反射材质,当海水的反射光线与视线夹角很小时,我们会感觉明显的反射高亮。而对于陆地来说,则不存在这种情况,陆地上的沙漠、草地、岩石、土地都基本不具有反射材质。所以若想我们的模拟更为真实,我们需要区分海水和陆地。而要做到一点,可以通过一个称为gloss map的方法来实现,也就是在位图的一个通道中(比如red通道)存储海水值(其实只有0和1,0表示陆地,1表示海水),本例中海水值存储于cloud.jpg的green channel中,随后在程序中,我们读取这个green channel的值并将其乘上反射光颜色(本例中为vec3(1.0, 0.941, 0.898))。图1.3的绿色区域表示海水。
(3)如果做到上面两点,那么你会我们模拟的地球仍然和真实的地球有差距——我们的地球没有云雾。在本例中,我们将云的纹理作为一个单通道(red channel)的纹理存存储(有点像做地形用的灰度图),如图1.3所示。这里所说的“纹理”实质上表现的是云层的厚度,在cloud.jpg的红色通道中,我们存储一个从0.0到1.0的值(1.0表示完全只看得到云,看不到下面的地表)。当处于白天时候时,我们希望云能够有散射光(也就是你能看得见云)而不反射光;而当处于夜晚的时候,我们希望完全看不到云,并且被云遮挡的地表也看不见。
图1.3 cloud.jpg(red通道表示云层厚度,green表示海水,blue没用)
二、顶点及片断着色器分析
(1)顶点着色器分析
本例的顶点着色器所计算的光照,与之前的简单纹理贴图中光照计算稍有不同,主要在于本例将散射光与反射光分开来进行处理(见代码1.1 中的 Diffuse和Specular变量 ),因为海水需要加入反射光而陆地不需要,简单的用一个LightIntensity无法满足本例的需要;同时因为本例用的是纹理贴图,也即散射光的颜色来自于纹理像素,所以对于散射光来说,只需要一个float型的变量来表示散射光强即可。顶点着色器代码具体如下:
// code 1.1 ( vertex shader ) varying float Diffuse; varying vec3 Specular; varying vec2 TexCoord; uniform vec3 LightPosition; void main() { vec3 ecPosition = vec3(gl_ModelViewMatrix * gl_Vertex); vec3 tnorm = normalize(gl_NormalMatrix * gl_Normal); vec3 lightVec = normalize(LightPosition - ecPosition); vec3 reflectVec = reflect(-lightVec, tnorm); vec3 viewVec = normalize(-ecPosition); float spec = clamp(dot(reflectVec, viewVec), 0.0, 1.0); spec = pow(spec, 8.0); Specular = vec3(spec) * vec3(1.0, 0.941, 0.898) * 0.3; Diffuse = max(dot(lightVec, tnorm), 0.0); TexCoord = gl_MultiTexCoord0.st; gl_Position = ftransform(); }
我们注意到顶点的纹理坐标通过TexCoord传递给片断着色器,即TexCoord = gl_MultiTexCoord0.st; 这里可能有疑问:即不是多即纹理贴图进行融合吗?怎么只有一个gl_MultiTexCoord0的坐标?查看一下图片即可知道,所有图片都是相同尺寸,也即三副图都共用一个纹理坐标,此处用一个TexCoord即可表达。
(2)片断着色器分析
a)如前面所说,cloud只有red、green通道是有用的,分别表示云的厚度及海水gloss,blue通道完全没用着,因为cloud的通道值在后面是需要用到的,所以这里第一步就是读取cloud纹理。
b)第二步计算出白天的颜色值,注意看下括号的位置:
vec3 daytime = (texture2D(EarthDay, TexCoord).rgb * Diffuse +Specular * clouds.g) * (1.0 - clouds.r) +
clouds.r * Diffuse;
首先我们将daytime纹理自身的散色光色与反射光色相加(如果处于海水处那么cloud.g为0,自然没有反射光),得到一个天晴状态下的地表色(也就没有被云遮挡),随后将这个值乘上(1.0 - clouds.r),因为clouds.r表示云层厚度,所以这个(1.0 - clouds.r) 可以看作是透过云层的光线强度,最后当处于白天时云是可见的,所以再加上clouds.r * Diffuse;
注意GLSL的语法,vec3 a = vec(1.0) 实质上等于 vec3 a = vec(1.0, 1.0, 1.0); 所以附加上云的颜色为白色。
c)第三步计算黑夜颜色: vec3 nighttime = texture2D(EarthNight, TexCoord).rgb * (1.0 - clouds.r) * 2.0;
这里就没有白天那么复杂,前面也讲了,因为是黑夜所以不需要加入云的光照计算,直接将nighttime纹理散射色与透过云层厚度的光强相乘即,这后面*2.0是想其黑夜颜色明显一些,可以去掉这个值或者换个别的值,自己试试。
d)目前我们的资源是白天纹理和黑夜纹理,那么什么时候用白天纹理,什么时候用黑夜纹理,当处于这两者之间的时间状态怎么办?我们必须得判断一下当前的时刻,这个前面也讲了,用diffuse值即可。当diffuse大于0时,处于光照状态,当diffuse等于0时,完全处于阴影之中,而当很接近0的时候,也即处于明暗界限的那个位置时,我们得融合两种纹理。下面的代码段就很好理解了:
if (Diffuse < 0.1)
color = mix(nighttime, daytime, (Diffuse + 0.1) * 5.0);
当小于0.1时,融合二者,diffuse可能为负值(diffuse是cos的值嘛,大于90度就为负了),这里的*0.5和+0.1同样也是为了效果更明显做了些微调。
uniform sampler2D EarthDay; uniform sampler2D EarthNight; uniform sampler2D EarthCloudGloss; varying float Diffuse; varying vec3 Specular; varying vec2 TexCoord; void main() { // Monochrome cloud cover value will be in clouds.r // Gloss value will be in clouds.g // clouds.b will be unused vec2 clouds = texture2D(EarthCloudGloss, TexCoord).rg; vec3 daytime = (texture2D(EarthDay, TexCoord).rgb * Diffuse + Specular * clouds.g) * (1.0 - clouds.r) + clouds.r * Diffuse; vec3 nighttime = texture2D(EarthNight, TexCoord).rgb * (1.0 - clouds.r) * 2.0; vec3 color = daytime; if (Diffuse < 0.1) color = mix(nighttime, daytime, (Diffuse + 0.1) * 5.0); gl_FragColor = vec4(color, 1.0); }