OpenGL笔记(八)

参考链接:https://learnopengl.com 作者 Joey de Vries

一. 几何着色器

几何着色器可以用来改变顶点着色器的primitive,比如在屏幕上画四个点,可以利用几何着色器将其改为任意图元。
下面举一个几何着色器的例子,屏幕上画了四个点,利用几何着色器可以将其改成四条线段:

#version 330 core
layout (points) in;         //顶点着色器输入的图元为点
layout (line_strip, max_vertices = 2) out;        //输出的图元为线段,最大顶点数为2

void main() {    
    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); 
    EmitVertex();          //将点1加入到新图元里

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);  //将点2加入到新图元里
    EmitVertex();
    
    EndPrimitive();//停止加点,形成新图元
}  

输入的in有五种大类型,值得注意的是,几何着色器传入的数据是从顶点着色器得到的,传入的数据类型,必须跟DrawCall里选择的类型相同,也就是与glDrawArrays()里面可以选的参数相同:

  • points: when drawing GL_POINTS primitives (1个点).
  • lines: when drawing GL_LINES or GL_LINE_STRIP (2).
  • lines_adjacency: GL_LINES_ADJACENCY or GL_LINE_STRIP_ADJACENCY (4).
  • triangles: GL_TRIANGLES, GL_TRIANGLE_STRIP or GL_TRIANGLE_FAN (3).
  • triangles_adjacency : GL_TRIANGLES_ADJACENCY or GL_TRIANGLE_STRIP_ADJACENCY (6).

输出的out图元只有三种:

  • points
  • line_strip:多个点连成的多段折线
  • triangle_strip:三角带

上面的gl_in是一个如下所示的结构体:

in gl_Vertex
{
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];  

由于In输入的图元可能不止一个点,所以gl_in是一个数组。
顶点着色器和片元着色器不用改:

#version 330 core
layout (location = 0) in vec2 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
}
#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(0.0, 1.0, 0.0, 1.0);   
} 

在main函数中,绑定和编译几何着色器:

geometryShader = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometryShader, 1, &gShaderCode, NULL);
glCompileShader(geometryShader);  
...
glAttachShader(program, geometryShader);
glLinkProgram(program); 

drawcall的函数不变:

glDrawArrays(GL_POINTS, 0, 4);  

最后可以得到这样的效果:
在这里插入图片描述

几何着色器输出的基本图元可以是带颜色的,不过这样就需要颜色数据从顶点着色器传到几何着色器:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

//顶点着色器中定义VS_OUT
out VS_OUT {
    vec3 color;
} vs_out;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
    vs_out.color = aColor;
}  

再在几何着色器中也加入对应结构体的定义,不过命名不能与上面的vs_out同名,

#version 330 core
layout (points) in;         //顶点着色器输入的图元为点
layout (line_strip, max_vertices = 2) out;        //输出的图元为线段,最大顶点数为2
out vec3 fClor;//传递到顶点着色器

in VS_OUT {
    vec3 color;
} gs_in[];  //可能同时接受多个图元点,所以这里是数组

void main() {    
	fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex

    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); 
    EmitVertex();          //将点1加入到新图元里

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);  //将点2加入到新图元里
    EmitVertex();
    
    EndPrimitive();//停止加点,形成新图元
}  

二. Instancing

OpenGL中,如果要绘制大量相同的model,比如绘制地图上的草,这些model的形状完全相同,可能只是大小与位置的区别,如果还用老模式,一个Model调用一个drawcall,势必会造成巨大性能消耗,为了解决这样的问题,OpenGL提供了这样的API:

glDrawArraysInstanced(..., ..., ..., int count);	//前面三个参数与glDrawArrays相同,后面参数为实例化的个数
glDrawElementsInstanced(..., ..., ..., int count);	//同上

但是单纯这样做并无卵用,因为绘制的model是一模一样的位置,所以还需要添加坐标信息到vertex shader
可以通过uniform,传递offset数组

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
	vec2 offset = offsets[gl_InstancedID];		//OpenGL内置的参数,用来标识instanced model的个数
	gl_Position = vec4(aPos + offset, 0.0, 1.0);
	fColor = aColor;
}

但通过uniform传递数组并不常见,因为uniform传递的内容数量有限,所以一般还是通过VAO的槽位进行数据传递。
创建该VBO,把其数据挖入对应的VAO中:

unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);  //transitions 是一个offset数组
glBindBuffer(GL_ARRAY_BUFFER, 0);

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);	
glVertexAttribDivisor(2, 1);        //唯一一句特别的地方

VBO和VAO的操作与正常操作一样,唯一不同的是多了一个API:

glVertexAttribDivisor(GLuint index, GLuint divisor);          //divisor是除数的意思

三. Anti Aliasing

(aliasing)锯齿,如下图所示,当放大图片后,经常会出现这种情况:

在这里插入图片描述
对于渲染过程,vertex的点可以是任意的,但片元确是在屏幕上的一块块的小型方形区域,区域的大小由分辨率决定,所以vertex的点与fragment的点并不能保证完全一一对应,因此会产生图像失真的情况,抗锯齿技术有以下几种:
在这里插入图片描述
主要分为两类:一种是使用高分辨率的方式(需要更好的性能和硬件),另一种是使用类似模糊边缘的方式。
下面介绍几种常用的:

  • Multisampling
    如下图所示,对于左下图的片元,取其中心点作为唯一的采样点,若该片元的中心点在三角形内,则该片元会被光栅化。
    在这里插入图片描述
    multisampling则设置了不止片元中心的一个采样点,如下图所示,可以设置4个采样点,左下图为原来的情况,当中心点不在三角形区域时,则直接判断片元的颜色为白色,右下图为Multisampling技术应用后的效果,设置四个采样点,若此时有两个点在三角形区域内,则根据四个点的颜色取平均值,也就是图中展示的淡蓝色:
    在这里插入图片描述

由此我们可以看出,Multisampling用了下述方法:
增设采样点,并对每个采样点的颜色取平均值,作为最终的片元颜色
那么,MSAA(MultiSampling Anti-Aliasing)是否意味着,对于上述情况,每个片元要计算其四个采样点的颜色,也就是进行四次四次计算呢?
答案是否定的,如果对每个片元的四个采样点都进行采样,会大大增加计算量,这样的消耗是计算机不能接受的,对于四个采样点,其像素值与片元中心点的像素值相同,额外需要判断的就是,在这四个点里,有多少个点在三角形以内即可。
如上图所示,通过计算中心点(红色点)的像素值A,对于四个采样点,两个采样点在三角形内,那么结果就是A* 2/4 = 0.5A,最终显示的颜色为淡蓝色。

下图给了个例子:
在这里插入图片描述
在这里插入图片描述
上图方框内的内容这一块不明白,如果取中心点的像素值,应该是0, 0* 1/4 = 0,所以不应该有颜色把 ???

OpenGL使用MSAA

  • 创建Window前需要调用glfwWindowHint(GLFW_SAMPLES, 4);,这样不仅colorbuffer, stencil buffer和depth buffer内存也会扩增
  • 开启抗锯齿功能glEnable(GL_MULTISAMPLE);(OpenGL默认是开启的)

离屏渲染使用MSAA
要使用离屏渲染输出MSAA处理后的贴图,稍微复杂一点,具体见https://learnopengl.com/Advanced-OpenGL/Anti-Aliasing

四. Bling Phong

之前讲过的Phong模型,在之前的教材中做过,当反射光线与人的眼睛的视线的夹角大于90°时,会自动把光照强度置0,这样会造成如图所示的光照骤然削减的情况(具体物理学上,大于90°角,人的眼睛到底能不能看到,我还没研究清楚):
在这里插入图片描述

为了加以改善,提出了改进的模型,Bling Phong光照模型,该光照模型不再计算反射光线,用了一个新的向量,叫做halfway vector,也叫半程向量,实际上就是入射光线和人的视线的角平分线的单位向量:
在这里插入图片描述
仅此而已,二者的区别就是光照强度的不同:

	float spec = 0.0;
    if(blinn)
    {
        vec3 halfwayDir = normalize(lightDir + viewDir);  
        spec = pow(max(dot(normal, halfwayDir), 0.0), 16.0);
    }
    else
    {
        vec3 reflectDir = reflect(-lightDir, normal);
        spec = pow(max(dot(viewDir, reflectDir), 0.0), 8.0);
    }

改进后的Blin Phong的优点:

  • 不会出现光线断层的情况
  • 直接算角平分线的计算量,会比通过OpenGL计算refect的反射向量的计算量更小,效率更高

值得注意的是,Blin Phong的cosθ会比原来的Phong式光照模型小,所有光照强度会更小,为了达到相同的效果,大约要将原有的shineness提高大约2到4倍。

五. Gamma校正

老式的CRT显示器有一个特性,当电压翻倍时,其亮度(指的物理亮度)呈幂指数级增长,这个幂指数约为2.2,这个值则为显示器的gamma指数,这个幂指数的增长趋势正好与人类对光线亮度的认知是一样的。

人类对于外界的感官程度是呈幂指数级增长的,比如一个房间内,打开一盏灯亮度为1,打开两盏灯亮度为2,那么亮度3的时候应该打开几盏灯?
为了保证亮度上升一个等级的时候,人的感官变化是相同的,在亮度为3的时候需要打开4盏灯。
下图是关于人的感官的亮度图,第一行是根据人类的感官分成的亮度的十个等级,而第二行是实际上的光的亮度的十个等级(基于单位区域光子的数量得到的):
在这里插入图片描述
可以看出,第二行的0.1等级的亮度,对于人的感官显得太亮了,线性变化不符合人类的感官认知,所以曲线不应该是线性变化,而是幂指数级变化,这个幂指数就是所说的gamma,曲线如图,为上升的幂曲线:

在这里插入图片描述
根据上图可以得知,如果在OpenGL中画一个图案,其颜色为暗红:

glColor = vec4(0.5, 0, 0, 0);

如果把它的红色度变为原来的两倍

glColor = vec4(1, 0, 0, 0);

代码上看出前后图案的红色度是两倍的关系,但是转换到我们的显示器上,因为显示器大都自带gamma校正,根据上述曲线,实际上的暗红色度是0.218,1/0.218 =4.5,所以实际上得到的是4.5倍的差距。

由上可知,从显示屏上看出的颜色对比,并不是真正的物理颜色值的对比,显示屏自带的gamma系数,会强行降低输入的亮度,导致整体效果偏暗。

比如说从外部导入一个模型,本该是正常显示,但是在显示器的gamma影响下,就会显得偏暗,不仅如此,当利用高级光线算法进行画面改善时,由于最后全给直接校正了,把颜色值x变为了x^2.2,这种一刀切,不按比例全部缩小的方式会很影响画面质量。
为了应对这个问题,有两个方案:

  • 在设置颜色的时候,把它调亮一些,这样在显示屏上显示就能正常一点,这个方案比较原始,现在并不好用。
  • 添加程序,给颜色进行反gamma曲线的亮度处理,这样再乘以显示器带的gamma,就能相互抵消。

第二个方案也就是我们说的Gamma 校正,下图是一个经过gamma校正的图,可以看出画面好了很多。
在这里插入图片描述

Gamma校正的具体做法
在输出color之前,对其进行相关运算,使其与显示器给与的gamma运算相抵消。显示器的gamma校正是把 x 变为了 x^2.2,如果要实现gamma校正,那么先把 x 变为 x^(1/2.2) 即可。

PS:大多数显示器的gamma值为2.2,也有的会略有不同,所以一般游戏里面都会允许你微调gamma校正的系数。
OpenGL实现gamma校正的两种方法:

  • By using OpenGL‘s built-in sRGB framebuffer support
  • Or by doing the gammac correction ourselves in the fragment shader(直接在 fragment shader里写)

sRGB是OpenGL的一个颜色空间,通过 enable GL_FRAMEBUFFER_SRGB会让OpenGL在每一次绘制前,将绘制的颜色先在sRGB空间内进行gamma校正,再把它存储到颜色buffer里。

glEnable(GL_FRAMEBUFFER_SRGB); //由硬件完成,gamma值大约为2.2,适用于大多家庭设备

也可自己手动计算,当fragment shader比较多时,建议在后处理阶段进行gamma校正

void main()
{
    // do super fancy lighting 
    [...]
    // apply gamma correction
    float gamma = 2.2;
    FragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}

注意: gamma校正必须在最后计算,否则会造成后面的计算错误,对于多个framebuffer的情况,只需要gamma校正最后一个frame buffer即可。

sRGB格式的贴图
sRGB是一个特殊的颜色空间,在这个空间里,将颜色值乘以二,意味着视觉上的双倍,而不是物理上的颜色倍率。
由于在创建贴图时,人们是基于显示器的颜色进行绘制的,这就意味着贴图都是在sRGB空间内绘制的。
如果不进行之前说的gamma校正,那么这个贴图显示肯定没问题,但是如果进行了gamma校正,会把本来就是正常的贴图的值进行校正,将x进行了x^(1/2.2)处理,则该值变大了,所以原本不需要变化的贴图,在gamma校正后会变亮,实际上贴图是被校正了两次
在这里插入图片描述

解决办法有两个:

  • 让美术在线性空间下制作材质,但实际上很多美术根本不知道sRGB的存在。
  • 对于贴图,进行反gamma校正,也就是把x变为x^2.2

关于第二条,解释一下,由于x是在sRGB空间下的,美术看着屏幕调出来的,所以应该是x^2.2是最终正确值。
移到OpenGL这边,如果全部做gamma校正,那么显示的值实际是,(x^(1/2.2) )^2.2 = x,这个值相较于x^2.2而言更大,所以是变亮了。
为了解决此问题,再加一个反gamma校正,也就是人为给贴图加一个gamma,就是((x(2.2))(1/2.2) )^2.2 = x^2.2,结果跟美术看的一样。

float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma)); //人为给贴图加一个gamma

这样一张张给贴图加gamma太麻烦,OpenGL有格式可以负责加载sRBG的图片,OpenGL会自动把它转换为线性空间:

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);  //不带alpha通道的
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);//带alpha通道的  

常见贴图对应的格式
diffuse texture一般是sRGB格式的,而specular maps 和normal maps 一般都是线性空间的,不能用作sRGB格式的贴图

用不用Gamma校正引发的光线衰减问题
物体世界,光线的衰减符合以下规律:

float attenuation = 1.0 / (distance * distance);

但是如果不用gamma校正,用线性的方程看上去更好

float attenuation = 1.0 / distance ;

因为显示器会做一个gamma运算,这样就符合distance的平方的物理规律了。
这个时候如果用以下公式,反而光线衰减的特别厉害:

float attenuation = 1.0 / (distance * distance);

如果gamma校正,那么还是用平方更好。

posted @ 2020-02-19 00:31  弹吉他的小刘鸭  阅读(145)  评论(0编辑  收藏  举报