openGL之glsl入门2--helloworld

    第一个程序还是简单一点的好,红宝书第一个例子,有两个地方比较难理解:

1. 用到了vgl.hLoadShader.h这两个头文件和相关函数,开始读这本书的时候,根本就不知道这两个头文件在哪里,你可以在配套代码目录oglpg-8th-editionincludelib目录下找到相关的头文件和实现。

2. 使用了vbovao,虽说是glsl编程比较基础的东西,但对初学者不好理解。

    这里写了一个自己的helloworld程序,让大家先能跑起来(完整源码在最后面),个人对helloworld的理解,尽量简单,也能看到一点效果,虽然这个程序只是窗口名叫helloworld。没使用缓冲对象,也没加载片元着色器(顶点着色器是必须的,片元着色器也是可选的,默认输出为黑色,所以本例子的小三角形是黑色的),应该可以直接跑起来,如果跑不起来,请大家检查自己的环境,运行的结果为:

openGL之glsl入门2--helloworld_第1张图片

    下面对程序每一部分做介绍。

1. main函数

    main函数没什么可说的,api自己看红宝书1.5.1章节。有一个细节需要注意:

    设置窗口大小及位置,需要放到窗口创建之前,否则这两句话会不生效,窗口默认创建在左上角。个人原来理解的是,先创窗口,后设置位置和大小应该也可以,很多UI程序都可以,但这里不行,这种小细节需自己注意。

    所以程序要精简的话,glutInitWindowPosition(200, 200); glutInitWindowSize(300,300); 这两句话也可以去掉的。

    类似有默认值的情况非常多,openGL很多种情况都有默认值,大家后续经常会碰到。

2. shader的加载

    init函数主要就是着色器的加载,这里只加载了一个顶点着色器,所有的几种着色器的加载流程都是一样的,都有以下几个过程:

1. glCreateProgram:创建program,所有着色器共用

2. glCreateShader:按类型创建shader

3. glShaderSource:关联shader的代码,可以同时关联多个shader程序字串,关联多个的时候第二个参数需对应修改。

4. glCompileShader:编译shader的代码

5. glAttachShader:把编译的shader依附到program上。

6. glLinkProgram:链接所有的shader程序

7. glUseProgram:启动program,这句话都可以删掉,没搞明白openGL是怎么默认处理的,输出一个白三角,貌似顶点生效了,片元未生效。

    shader的加载是操作GPU的第一步,只有先把程序告诉GPUCS模型中的服务器),才能安排程序执行,不像CPU,可以从硬盘,U盘,网络多个渠道获取执行文件,它就这种方式来获取可执行文件。

    shader有加载也有卸载,glUseProgram 参数传0的话,将清除所有的shaderopenGL里有很多函数都是这么用的,传0表示清除,用的时候需注意别传错了,简单的程序可以固定不用动。 获取加载信息的函数有glGetProgramivglGetProgramInfoLog等。

    例子里着色器的代码使用字符串的方式加载的,如果shader程序短的话,还比较方便,程序复杂的话,这种方式就不适用了,大家可以参考红宝书例子中的LoadShader函数的实现,从文件中load shader程序。

 

注意:加载shader的每一个过程都可以获取执行的状态值,这里为了大家看的清爽,错误处理都删了,实际的程序这些东西必须要有的,否则shader加载报错后(如shader程序语法错误),窗口默认就是一片黑,看不到任何错误,一片黑的情况有很多种,没处理好,增加排错难度(GLSL程序本身就难调)。

 

3. 顶点着色器

    vertex_source定义的是顶点着色器的内容,这段代码是在GPU中执行的,数据来源是CPU传送过来的。

layout (location = 0) in vec4 position;

    上面中的layout location = 0 ),这句话不是很好理解,不用管布局限定符layout,关注点在这个0上,这就是一个约定,这个0glEnableVertexAttribArray的参数index必须一致,你可以把0改成其他值如2,一样可以运行,但一定要对应,如下:

layout (location = 2) in vec4 position;
glEnableVertexAttribArray(2);   
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid *)vertices);

    这个就是CPUGPU约定的交互句柄,对应上就可以用,老版本比这个复杂,需要通过名称去获取index,类似glEnableVertexAttribArray(getindex("position"))的方式来调用。

    这里用到了关键字in,表明这个position变量是用来接收数据的,这种限定词常见的有inoutuniform,本节不详细讲(后面引入uniform时 再细说),有一点需注意,有时候会看到关键字AttributeVarying,这个是老版本glsl用的关键字,现在不用了,与inout作用类似,都是用来传入传出数据的。

    vec44维浮点向量,表明传入的4维向量,但我们程序传入的是二维的,这里接收的4维是没有问题的,因为vec4有默认值,默认默认值为(0001),我们传入的2维的坐标,后面的zw默认为0.01.0,所以shader的代码也可以这么写:

layout (location = 0) in vec2 position;
gl_Position = vec4(position .x,position .y,0.0,1.0);

也可以

gl_Position = vec4(position ,0.0,1.0);

    使用2维向量传进来,通过vec4这个构造函数把他转成4维的,上述两种构造方式都可以,默认值我们自己定。w的值不要改,z的值这个程序你改了也没用,看不出效果,因为openGL的坐标系默认Z轴垂直于屏幕,正对着你,只有旋转过后才能看的出效果。

    也可以改成如下的代码,可以做简单的平移与缩放,但这么做不灵活,一般使用矩阵相乘的方式实现。

gl_Position = vec4(position .x-0.5,position .y-0.5,0.0,1.0);
gl_Position = vec4(position .x*2,position .y*2,0.0,1.0);

    关于4维向量vec4,大家可以在红宝书2.3.1章节里面详细了解,vec4成员除了用(x,y,z,w)方式引用外,也可以使用(r,g,b,a)(s,t,p,q),对于同一个向量,使用3种方式的任何一种方式引用都是一样的,只不过为了设计与代码阅读,分别用在顶点、颜色、纹理不同场景。上述代码也可以写成

gl_Position = vec4(position .r,position .g,0.0,1.0);  /* 实际效果一样,只不过看着别扭 */

    全局量gl_PositionGLSL定义的全局变量(内置变量),用来把输入的坐标转成顶点坐标,初学的话其他的暂先不用了解,类似的全局量比较多,可以参考红宝书附录C

    GLSL的语法与cC++比较类似,学起来应该没什么障碍,严格而又灵活,很有意思,大家可以边练边学,上手还是比较容易的。

 

注意:gl_Position对应的是一个顶点的显示位置,glDrawArrays会告知需要绘制顶点的个数,个人理解shader程序是针对单个点的,glDraw 绘制N个顶点,shadermain函数会执行N(GPU会并行执行,不是循环执行),点与点之间的没有关联,应该干不了根据当前点的坐标修改下个点坐标的事情。

 

4. 数据传送与绘制

    数据传送与绘制由以下3个函数实现的:

void glEnableVertexAttribArray(GLuint index);
void glVertexAttribPointer(GLuint index,GLint size, GLenum type, GLboolean normalized,GLsizei stride,const GLvoid * pointer);       
void glDrawArrays(GLenum mode,GLint first,GLsizei count);

1. glEnableVertexAttribArray启用顶点数据array,上面已经讲了,index值与shader的location值要保持一致。

2. glVertexAttribPointer用来设置传送数据状态,size表明你传的顶点数据是几维的,例子中6个浮点数,表示3个坐标,只传了xy的坐标,所以例子要传2stride表示每取一个点,需向后偏移的长度。

3. glDrawArrays表示绘制方式,mode参考红宝书3.1,count表示需绘的点的个数,我们只传了3个点,所以这里为3

    glEnableVertexAttribArrayglVertexAttribPointer可以互换位置。glDrawArrays要放到后面,只有先告知怎么绘并传送了数据,才能绘制。

    大家可能注意到了,glDrawArrays的时候并没有传入坐标数据,而是glVertexAttribPointer传入的,实际上是glVertexAttribPointer函数把绘制信息与绘制数据信息缓冲起来了,可以理解为存到了全局变量中(openGL中叫状态),glDrawArrays调用的时候直接用前面设置的信息。

    没有使用vbo时,程序效率是比较低的,当调用glDrawArrays 的时候才会把数据传送到GPU,而不是glEnableVertexAttribArray 函数调用时传送到GPU的,大家可以做一个实验来验证,把glEnableVertexAttribArrayglVertexAttribPointervertices 数组移到init函数中 ,程序如下:

void init()
{
    GLuint program = glCreateProgram();
    loadShader(program,GL_VERTEX_SHADER,vertex_source);
    glLinkProgram(program);
    glUseProgram(program);
    glClearColor(0.5f,0.5f, 1.0f, 1.0f);
    GLfloat vertices[] = {0.0, 0.0,0.5,0.5,0.5,0.0};
    glClear(GL_COLOR_BUFFER_BIT);
    glEnableVertexAttribArray(0);   
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid *)vertices);
}
void display()
{
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
    glFlush();
}

    运行的结果是看不到小三角形,可以把GLfloat vertices[]数组提到函数外面,变成全局量,此次就可以绘制出小三角,原因是上面的代码vertices为局部变量,glDrawArrays传送vertices数据到GPU,此时,缓冲的vertices数组的指针已经失效(栈地址),提到外面(全局变量),就不存在这个问题了。这种细节(状态与数据传递过程)大家可以好好琢磨一下,只有理解了,才能理清楚函数的关系,哪些应该放前面,哪些放后面。

 

注意:glVertexAttribPointer有两种用法,使用vbo与不使用vbo参数pointer的含义是不一样的:

1. 不使用vbo,例子就是这种情况,pointer直接指向数据的首地址。

GLfloat vertices[] = {...};
glVertexAttribPointer(eVertex, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid *)vertices);

2. 使用vbo,先需要申请,并绑定数据,pointer可以直接传空。

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, NULL);

    stride指的数据对齐方式,以字节为单位,一般可以直接传0,传0表示数据紧密排列,每次取数据会按数据类型*数据宽度进行偏移,如果不想默认方式处理,这个值一定要传对,否则结果会奇奇怪怪的。

 

5. 背景与刷新

    主要涉及两个函数:

glClearColor(0.5f,0.5f, 1.0f, 1.0f);     //期望背景清成什么颜色,不调用的话,默认为黑色.
glClear(GL_COLOR_BUFFER_BIT);     //实际把背景清成什么颜色

    这两个函数需配合着用,glClearColor不是必须的,英文我们没使用片元着色器,默认画的东西是黑色的,如果不设置背景,背景默认也是黑的,就看不出效果,所以这里设置了一下。

    glClearColor可以只调一次,相当与设置了一个状态(把颜色值放到全局量中,供glClear 使用),大家可以尝试着把glCleardisplay函数移到init函数,放在glClearColor后面,这样也是可以显示出来的,但当你拖动改变窗口大小时,差别就出来了,一般刷新的时候,背景都需要刷。

    拖动改变窗口大小时,display函数会重新执行一次,这就是重绘,触发重绘的方式有好几种,这是一种情况,大家可以在display函数中加打印,每次改变窗口大小时,都会执行一次。

    glFlush函数是把以上设置的信息立即传送到GPU上,不调用的话就是一片白,进行刷新的有多种方式,后面详解。

 

    学习openGL就是要多练,光看不练有很多地方难理解透,本文的例子比较简单,也说了一些可以换位置的内容,大家可以多折腾,换换参数,换换代码位置,也不用担心出问题,先把简单的整明白,后续会少些坑。

 

完整代码如下:

#include 
#include 
static const GLchar * vertex_source =
    "#version 330 core\n"
    "layout (location = 0) in vec4 position;\n"
    "void main()\n"
    "{\n"
    "gl_Position = position;\n"
    "}\0";
void loadShader(GLuint program,GLuint type,const GLchar * source)
{
    const GLchar * shaderSource[] = {source};
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, shaderSource, 0);
    glCompileShader(shader);
    glAttachShader(program, shader);
}
void init()
{
    GLuint program = glCreateProgram();
    loadShader(program,GL_VERTEX_SHADER,vertex_source);
    glLinkProgram(program);
    glUseProgram(program);
    glClearColor(0.5f,0.5f, 1.0f, 1.0f);
}
void display()
{
    GLfloat vertices[] = {0.0, 0.0,0.5,0.5,0.5,0.0};
    glClear(GL_COLOR_BUFFER_BIT);
    glEnableVertexAttribArray(0);   
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid *)vertices);         
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 3);
    glFlush();
}
int main(int argc, char * argv[])
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGBA);
    glutInitWindowPosition(200, 200);
    glutInitWindowSize(300,300);
    glutCreateWindow("HelloWord");
    glewInit();
    init();
    glutDisplayFunc(display);
    glutMainLoop();
    return 0;
}


 

 

 

你可能感兴趣的:(openGL)