OpenGL step by step - tutorial_4 "hello shader"

从这节开始,之前的每一个效果和技术我们都将使用shader实现。shader是做3d图形的现代方式。你可能抱怨这种方式是一种退步,当大多数3d设计是固定管线实现并且只要求开发者指明配置参数(光照属性,旋转值等),现在却要开发人员通过shader编程实现,但是,这种可编程特性创造了很好的灵活性和技术革新。

OpenGL可编程管线可抽象成如下步骤:


顶点处理机vertex processor负责在每个通过管线的顶点上执行vertex shader。vertex shader并不识别渲染图元的拓扑结构。此外,在vertex processor中你也不能丢弃(discard)顶点。每个顶点进入vertex processor一次,经过转换,将沿着管线继续向下进行。

下一步是几何图形处理器geometry processor。在这一阶段,完整图元(例如所有的顶点)的认知以及临接顶点将提供给shader。这enable了一些技术,必须考虑顶点之外的其他信息。geometry shader还能切换draw call中设置的拓扑为另一种不同的拓扑结构。例如,你提供一份点的列表,并生成两个三角形(比如一个四边形)从每一个点(billboarding布告牌技术)。另外,你也可以提供多个顶点multiple vertices给每个geometry shader调用,然后根据你设置的输出拓扑生成多个图元。

下一步是裁减。这是一个固定功能单元,任务简单直接——把图元裁减到前一个教程中我们看到的那个规格化框内,同时也裁减到近z平面和远z平面的范围。在这里也可以应用用户自定义裁减平面并使用裁减器裁减。裁减后留下的位置顶点投影到屏幕坐标系,光栅器按设置的拓扑结构将他们绘制到屏幕。例如,在处理三角形时这意味着找出这个三角形内所有的点,光栅器为每个点启动片元处理器fragment processor,在这里你可以决定该点像素的颜色通过在texture上采样或其他技术。

光栅化阶段和片元操作阶段,clipper后的三角形会先被光栅化,产生很多的fragment(片元),接着执行片元shader,在片元shader中,可能会装入纹理,从而产生最终的像素颜色。 【fragment可以理解为带sample、深度信息的像素】

这三个可编程阶段(vertex,geometry,fragment processor)是可选的。如果你没有向他们绑定shader,那么默认的固定管线功能将被执行。

shader的管理与C/C++程序的创建很相似。

首先,写shader文本程序,并使其在程序中可用。实现方法可以是:把这个文本程序存储在源码中的一个字符串数组中,或者从外部文本文件读入(最后还是读存入一个字符数组)。

然后,然后编译该shader代码,把编译后的shader代码逐个读入到各自的shader对象中。

接着,把这些shader链接到一个单独的program,最后把shader载入GPU。链接这些shader时使得驱动有机会修剪和优化他们根据他们之间的关系。例如,你可能有一对顶点着色器和片元着色器,在顶点着色器中生成了法线,但是在片元着色器中根本就没使用它,此时驱动中的GLSL编译器就把这个法线相关功能就移除了,使得顶点着色器更快的执行。如果之后那个顶点shader又和一个使用发现的片元shader成对了,那么链接到里一个program中时又会生成另一个不同的顶点shader。


主要代码实现步骤:

首先创建一个shader程序对象,然后分别创建相应的shader对象,比如顶点shader对象, 片元shader对象,并把它们连接到shader程序对象。在创建shader对象时候,需要装入shader源码,编译shader,链接到shader程序对象几个步骤。

GLuint shaderProgram = glCreateProgram();

//首先为shader创建一个程序对象program object,之后我们将把所有shader链接到这个程序对象。

GLuint shaderObject = glCreateShader(ShaderType);

//我们将使用这个函数创建两个shader,vs和fs。其中一个的shaderType是GL_VERTEX_SHADER,另一个是GL_FRAGMENT_SHADER。

指定shader源码和编译shader的步骤对于这两种shader来说是一样的:

const GLchar* p[1];

p[0]=shaderText;

GLint Length[1];

Length[0]=strlen(shaderText);//由于glShaderSource对参数类型的要求才有了这四行

glShaderSource(shaderObject,1,p,Length);

//(shader源码绑定对象,shader源码数组个数,源码字符数组,字符个数)

//在编译shader之前我们必须要指定每种类型shader的源码。函数glShaderSource使用shader对象作为参数,为指定shader源码提供了灵活性。shader源码可以分布在在多个字符数组中(例如一个shader却每行代码存一个数组中),这样你就得提供一个存储这些分布数组地址的指针数组,以及一个整型数组,该整型数组存储每个分布数组中字符的个数。为了简洁,我们使用一个数组储存整个shader源码,这样源码指针和其长度就只有一个参数,函数中第二个参数就是这两个数组(源码数组指针,字符个数数组指针)内元素的个数。

glCompileShader(shderObject);

// 调用shader对象从而对shader进行编译,这个步骤很简单......

GLint success;

glGetShaderiv(shaderObject,GL_COMPILE_STATUS,&success);

if(!success){

GLchar InfoLog[1024];

glGetShaderInfoLog(shaderObject,sizeof(InfoLog),NULL,InfoLog);

fprintf(stderr,"Error compiling shader type %d:'%s'\n",shaderTyle,InfoLog);

}

//为了便于shader调试,这几行代码获取shader的编译状态并显示编译时出现的错误。

glAttachShader(shaderProgram,shaderObject);

//最后,把编译后的shader对象和程序对象绑定。

glLinkProgram(shaderProgram);

//在经历编译shader,将shader绑定到程序后我们来链接程序。注意,在链接了程序后你就可以使用glDetachShader和glDeleteShader来释放中间结果的shader对象了。OpenGL驱动维护者一个它生成的对象的引用计数。如果创建一个shader对象之后又删除了它,驱动就会释放它,但是如果该shader对象绑定到了一个程序,那么调用glDeleteShader就只能是标记它作为待删除对象并且还要调用glDetachShader函数,这样该对象的引用计数清为零并且移除该对象。

success=0;

glGetProgramiv(shaderProgeam,GL_LINK_STATUS,&success);

if(0==success){

glGetProgramInfoLog(shaderProgram,sizeof(ErroLog),Null ,ErrorLog);

fprintf(stderr,"Error linking shader program:'%s'\n",ErrorLog);

}

//以上代码用来检测程序连接的状态并输出错误信息。注意其中函数名称的差别与shader编译的相比。

glValidateProgram(shaderProgram);

//为什么在成功编译后还要确认程序validate the program呢?与链接的区别在于,链接的操作还是基于绑定shader对象的,而确认程序则是检测该程序是否可以在当前管线状态下执行。在一个复杂的应用中会有多个shader和多个状态,所以最好在每次绘制前确认一下。在我们这个简单的程序中确认一下就行了。当然,为了减少验证的开销,我们可以只在debug阶段进行验证操作,而最终的release阶段不需要这些操作,从而减少程序开销,提高性能。

glUseProgram(shaderProgram);

//最后,使用这个函数把链接后的程序对象加入到管线状态中。这个程序对象将对之后的绘制函数一直有效知道你用其他程序对象替换了它或使用过来UseProgram(NULL)禁用了它(这就激活了固定管线)。如果你自创建了一个只包含一种shaderType的shader程序,那另外一个阶段的操纵就气启用默认的固定管线功能。


关于shader管理的就这些了,下面说一下顶点shader和片元shader的内容。

#version 330

//通知编译器,我们使用的是GLSL3.3版本。如果编译器不支持那就报错。

layout(location=0) in vec3 Position;

//这个声明会出现在顶点着色器内。它声明了一个顶点具体的属性名‘Position’,有三个float的向量。‘顶点具体的属性’意味着:GPU每次调用shader,buffer中每个新顶点的这个属性值都赋给它。这个声明的第一部分layout(location=0),创建了属性名Position和buffer内属性的绑定关系。 location指定该属性在顶点shader中的位置,适用于我们的顶点数据里包含多个属性的情况(同时包含位置,法线,纹理坐标等)。我们必须要让编译器知道buffer中顶点数据的哪个属性绑定到这个声明的属性名上。有两种方法:1.就像这个声明一样,显示声明Position这个属性名的location是0,这时在应用程序中我们就使用这个固定值(比如代码中的glVertexAttributePointer(0));2.或者省略第一部分,直接在shader中声明为‘in vec3 Position’,然后在应用程序中使用glGetAttribLocation(name)查询其在shader中的location值,然后再把这个返回值应用到glVertexAttributePointer(value)。在这里我们使用了简单的方法,也就是第一种,但是在更复杂的程序中还是让编译器决定属性的索引吧,然后在运行时动态查找,这样可以方便把多个shader源文件集成在一起,而不必用它们匹配我们自己定义的缓冲layout。

void main();

//我们可以把多个shader连接在一起组成一个shader对象,但是,每个shader阶段(VS,GS,FS)有且只能有一个main函数,它是shader的入口。例如,你可以创建一个光照函数库文件并连接到你的shader,但是这个库文件中不能有main函数。 For example, you can create a lighting library with several functions and link it with your shader provided that none of the functions there is named 'main'。

gl_Position=vec4(Position.xy*0.5,Position.z,1.0);

//这里我们使用固定值去转换传入的顶点的位置。‘gl_Position’是一个内建变量,用来存储定点的齐次坐标。光栅器会查找这个值,并用做屏幕空间的位置坐标(也会伴随一些变换)。注意,我们设置w分量为1.0,这对能否正确显示这个图像是十分重要的。把3d投影到2d是分两个步骤完成的:首先,每个顶点乘以投影矩阵(我们会用几个教程来讲解);然后,GPU自动为位置属性执行透视除法(gl_Position里的分量xyz除以w分量)在其到达光栅器之前,在本例shader中我们没有任何投影计算但是透视除法阶段我们是无法跳过的。

如果运行正确,到达光栅器的三个顶点会是 (-0.5, -0.5), (0.5, -0.5) , (0.0, 0.5)。因为这些点都在规格化框内所以并不会执行裁减。这些值投影到屏幕坐标系(视口变化操作),光栅器遍历这个三角形内的每个顶点,对每个顶点执行片元着色器 。下面就是片元着色器内的代码:

out vec4 FragColor;

片元着色器的工作是决定每个片元或像素的颜色。另外,片元着色器还可以丢弃片元或像素,或者改变z值。上面声明的out变量负责输出颜色值,其四个分量代表RGBA。这个变量的值会被光栅器接收,最终写入帧缓存。

FragColor = vec4(1.0,0.0,0.0,1.0);

前面的教程中并没有片元着色器,所以图像都是默认的白色,这里我们把它设置成了红色。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值