一、Blinn-Phong model
- 冯氏模型的镜面反射采取了观察方向与反射方向的夹角,这样会造成当夹角大于九十度时,就会造成镜面反射光为零,这样一般情况下问题不大,但是当镜面反射的glossy程度比较大时,就会出现镜面反射边缘的截断现象。
- Blinn-Phong model改用半程向量与法线的夹角,解决了问题,其他方面一样。
//specular
vec3 viewDir = normalize(viewPos - fragPos);
vec3 halfwayDir = normalize(viewDir + lightDir);
float spec = pow(max(dot(halfwayDir, normal), 0.0), material.shininess);
vec3 specular = light.specular * spec * vec3(texture(material.specular, texCoord));
二、Gamma矫正
- 在Gamma校正之前,首先需要理解什么是线性颜色空间。我们为什么要强调线性空间呢?顾名思义,线性空间就是指可以对颜色进行线性操作还能得到正确结果的颜色空间。事实上,线性空间只有一个,即自然空间。在自然界中,我们以光子的数量或者光的强度来描述亮度,而这个映射关系是线性的,即亮度与光子数量呈线性关系,那么对亮度的先线性操作就具备充分的物理意义。当然,这里对亮度的概念是一个定义,并非观感上的亮度,即观感亮度与光子数量是非线性的。
- 了解了线性亮度空间之后,我们就可以知悉,如果要对颜色进行计算,在模拟自然光照时,需要在线性空间进行计算才有直观的意义。
- 另外一个需要强调的点是,自然界的亮度,即光子数量,经过人的视觉系统的转换后,会呈现出亮度认知,而这一个过程是非线性的,大概会是1/2.2的幂次关系。这样一来,人会对暗区更加敏感,即人的观感亮度是非线性且偏亮的。应该认识的是,人的视觉系统是一个物理系统,将电磁波转换为电磁波或者是说将光子转换为电子的过程,这可以结合到后面的CRT显示器。
- CRT显示器是将电转换为光的物理设备,是一个与人的视觉系统反向处理的设备。这个设备的转换过程同样不是线性的,大致是2.2的幂次关系,这就意味着线性颜色空间经过CRT之后变成了偏暗的非线性空间。
- 结合人的视觉系统与CRT之后,一个直观的印象是,二者的非线性不是正好大致抵消,变成了线性了吗,这样一来,计算机中以线性方式计算的亮度值转换到人的视觉感知系统中的表达方式不也恰好是线性空间了吗?事实上,这种思路是错误的,因为在前面提起了,人的视觉系统的颜色表达空间是非线性的,而人类认知颜色亮度是从一个线性的自然空间认知的。因此,偶们要保证投射入人眼的光照形式的亮度空间必须是一个线性空间,恰如自然界。
- 结合:我们需要在计算机中以线性空间进行颜色计算 + CRT显示器的输出光的颜色空间必须是线性的,那么直观的结论就是,我们需要将计算出来的线性空间的颜色表达经过一步大约是1/2.2次幂的校正,然后送入显示器的光电转换设备,才能恰好得到线性空间,而人眼也可以观察到合理的亮度了,这一步便是Gamma校正。
- 在OpenGL中,实现Gamma校正有两种方式:
glEnable(GL_FRAMEBUFFER_SRGB);
这个使得opengl再写入颜色缓冲时会自行进行gamma校正,将线性空间转换为sRGB空间,sRGB这个颜色空间大致对应于gamma2.2。这样一来,我们的shader只需要在线性空间做计算,不需要进行gamma矫正了。显然的是,这样使得我们只有使能权力,没有控制权力。- 另一个方法就是不开启GL_FRAMEBUFFER_SRGB,而是在片段着色器中自行进行gamma校正,然后让opengl写入RGB空间,相当于得到了sRGB空间。但是明显的是,我们有了更细粒化的操作权限。
//gamma
float gamma = 2.2;
fragColor.rgb = pow(fragColor.rgb, vec3(1.0 / gamma));
看看,多么亮!
- 一个明显的问题就是,经过了gamma校正的场景也太亮了吧,这是为什么呢?这是因为,我们观察到的场景中基本上都是基于纹理贴图渲染出来的,而纹理贴图并不是线性空间。纹理贴图是如何绘制出来的呢?显然是,艺术家们对着显示器调节出来的。那么显然的是,在艺术家的眼中,纹理贴图的亮度是正常的,即纹理贴图经过显示器后的亮度是线性空间的,那么显然的结论是,纹理贴图中记录的颜色本身不是线性空间的颜色,而是已经经过了gamma校正的sRGB空间的颜色。那么,我们将纹理映射出来的颜色进行gamma校正,就相当于进行了两次gamma校正,自然亮度会郭亮,反之可以想到,再不惊醒gamma校正时,纹理的颜色自行经过了gamma校正,所以显示的是合理颜色。然而,如果我们要对颜色进行计算,就意味着直接使用纹理贴图的sRGB颜色的计算结果是错误的。因此,对于纹理贴图,我们需要首先将她们由sRGB空间转换到线性空间。
- 第一种方法就是,我们在片段着色器中使用texture()提取到颜色后,首先经过一个反gamma校正。
float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));
- 另一个比较方便的方法是,我们在使用glTexImage2D读取纹理数据时,通过将内部格式设置为GL_SRGB或者GL_SRGB_ALPHA来告知OpenGL将其由sRGB或者sRGBA转换为RGB或者RGBA
glTexture2D(GL_TEXTURE_2D,0,GL_SRGB,width,height,0GL_RGB,GL_UNSIGNED_BYTE,data);
- 然而,需要注意的是,只有颜色纹理需要经过反gamma校正,其他纹理像是深度纹理、法线纹理等等不存在类似的需求。同样的,我们在创建帧缓冲时,在为其纹理缓冲分配空间时也不需要进行转换,毕竟我们存入的颜色本身就是线性空间计算出来的结果。
int width, height, nrChannels;
unsigned char* data = stbi_load((dir+'/' + pat).c_str(), &width, &height, &nrChannels, 0);
if (data)
{
GLenum interformat,format;
if (nrChannels == 1)
{
interformat = GL_RED;
format = GL_RED;
}
else if (nrChannels == 3)
{
interformat = GL_SRGB;
format = GL_RGB;
}
else if (nrChannels == 4)
{
interformat = GL_SRGB_ALPHA;
format = GL_RGBA;
}
glTexImage2D(GL_TEXTURE_2D, 0, interformat, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture." << std::endl;
}
stbi_image_free(data);
- 最后一个需要注意的地方是,当不采取gamma校正时,使用距离反比的光照衰减效果会很好,反而平方反比会使得光照衰减很快,这是因为显示器的gamma参数导致的距离反比更接近真实的平方反比,显然的,使用gamma校正之后,距离平方反比的效果更好。