使用GLSL画点,画线,画面,与原来使用glBegin(), glEnd()的方式有类似的地方,原来方式api比较多,GLSL采用的数组一次传送,程序的本质还是坐标点的设计与确认,只要知道图怎么画,哪种方式差异不大,本章主要介绍:
1. 正弦函数的基本画法
2. 键盘的控制
3. uniform变量的用法
4. 正弦波叠加为方波的GLSL实现
GLSL画这些基本的类型是,使用的函数主要是glDraw*系列的函数,这里再说一下:
void glDrawArrays (GLenum mode, GLint first, GLsizei count);
mode与老的方式一致,有以下类型,画点GL_POINTS,画线 GL_LINES,顺连线段GL_LINE_STRIP,回环线段GL_LINE_LOOP,三角形GL_TRIANGLES,GL_TRIANGLE_STRIP,GL_TRIANGLE_FAN,四边形GL_QUADS,GL_QUAD_STRIP,多边形GL_POLYGON。first为0即可,count表示要绘制顶点的个数,这个上章说的比较多。
点面的画法的例子比较多,请大家自己去练(可以找老的程序改用GLSL方式实现来练手)。
说明:严格的说,以上关于新老绘制方式的描述并不准确,openGL绘制方式一直在改进,这里主要表达的意思是大家需区分使用shader的方式绘制与直接绘制方式的异同,详细的绘制演进过程,可以看一下这篇文章的介绍:
上一章的helloworld程序非常简单,这里通过正弦波的画法来实现一个稍微复杂一点的shader程序,让大家尽快感受shader编程。
1. 正弦波绘制
正弦波公式y = sin(x ) ,公式大家都熟悉,怎么画出来呢?这里我确定了以下几个参数来画正弦波:
1. sampleCnt:采样点个数,openGL画东西都采用逼近的方式,采样点越多,正弦波就越精细。
2. factor:用来控制正弦波的频率,如sin(2x ),sin(3x ) 等。
3. amplitude:振幅,用来控制正弦波的振幅,如3sin(2x )。
4. rangeL:我们要把正弦波映射到[-1.0,1.0]的范围内,否则画出来的正弦波都看不到。
5. rangeR:如传的-pi~pi的范围,会把这个范围的正弦波映射到[-1.0,1.0]范围内。
注意:shader里面y坐标统一乘了0.9,主要是避免图形顶到边框,代码如下,可以改参数效果:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <GL/glew.h>
#include <GL/glut.h>
#define PI 3.14159265
#define SAMPLE_CNT 200
typedef struct
{
GLfloat x;
GLfloat y;
}Point;
static const GLchar * vertex_source =
"#version 330 core\n"
"layout (location = 0) in vec2 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x,position.y*0.9,0.0,1.0);\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);
}
Point * createSinArray(GLint sampleCnt,GLfloat factor,GLfloat amplitude,GLfloat rangeL,GLfloat rangeR)
{
int i = 0;
GLfloat range = rangeR-rangeL;
Point * array = NULL;
if((sampleCnt <= 4) || (rangeR <= rangeL))
{
printf("param error sampleCnt:%d rangeR:%f rangeL:%f\n",sampleCnt,rangeL,rangeR);
return NULL;
}
array = (Point * )malloc(sampleCnt * sizeof(Point));
for(i = 0;i<sampleCnt;i++)
{
/* x坐标按采样点均匀的分布在[-1.0,1.0]的范围内*/
array[i].x = (2.0*i-sampleCnt)/sampleCnt;
/* y坐标考虑到了振幅,频率因素的影响*/
array[i].y = amplitude*sin(factor*(rangeL+i*range/sampleCnt));
//printf("array[%d]:%f-%f\n",i,array[i].x,array[i].y);
}
return array;
}
void deletSinArray(Point * array)
{
if(array)
{
free(array);
}
}
void display()
{
int i = 0;
glClear(GL_COLOR_BUFFER_BIT);
Point * sinaArray = createSinArray(SAMPLE_CNT,1.0,1.0,-3*PI,3 * PI);
if( sinaArray)
{
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,sizeof(Point), (GLvoid *)sinaArray);
glEnableVertexAttribArray(0);
glDrawArrays(GL_LINE_STRIP, 0, SAMPLE_CNT);
deletSinArray(sinaArray);
}
glFlush();
}
int main(int argc, char * argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutInitWindowPosition(200, 200);
glutInitWindowSize(300,300);
glutCreateWindow("article3");
glewInit();
init();
glutDisplayFunc(display);
glutMainLoop();
return 0;
}
结果如下:
2. 按键捕获
每次改参数需要编译才能看效果还是很不方便,下面的例子加入了键盘控制,并加入了正弦波合成方波的处理,可以使用箭头键移动正弦波,使用上下箭头进行振幅调整,使用+,-号来调整正弦波叠加的次数。
傅里叶函数分解方波公式:
f(y) = 4/PI * (sinx+ sin3x/3 + sin5x/5 + ...);
实际程序里面公式为:
f(y) = sinx+ sin3x/3 + sin5x/5 + ...
学习GLSL的同时,顺便来熟悉一下傅里叶函数,想起一句话,只要努力,弯的也能掰成直的(咳,不是我说的,有兴趣的可以搜一下傅里叶掐死教程)。键盘输入捕获主要使用一下两个函数:
void glutKeyboardFunc(void(*func)(unsigned char key,int x,int y));
void glutSpecialFunc(void (*func)(int key,int x,int y));
glutKeyboardFunc能捕获普通的按键(数字,字母),而glutSpecialFunc用来捕获方向键,F10等特殊键,这两句话加到glutMainLoop之前就可以了。
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <GL/glew.h>
#include <GL/glut.h>
#define PI 3.14159265
#define SAMPLE_CNT 200
typedef struct
{
GLfloat x;
GLfloat y;
}Point;
static const GLchar * vertex_source =
"#version 330 core\n"
"layout (location = 0) in vec2 position;\n"
"uniform mat4 matrix;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x,position.y*0.9,0.0,1.0);\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);
}
Point * createSinArray(GLint sampleCnt,GLfloat factor,GLfloat amplitude,GLfloat rangeL,GLfloat rangeR)
{
int i = 0;
GLfloat range = rangeR-rangeL;
Point * array = NULL;
if((sampleCnt <= 4) || (rangeR <= rangeL))
{
printf("param error sampleCnt:%d rangeR:%f rangeL:%f\n",sampleCnt,rangeL,rangeR);
return NULL;
}
array = (Point * )malloc(sampleCnt * sizeof(Point));
for(i = 0;i<sampleCnt;i++)
{
array[i].x = (2.0*i-sampleCnt)/sampleCnt;
array[i].y = amplitude*sin(factor*(rangeL+i*range/sampleCnt));
//printf("array[%d]:%f-%f\n",i,array[i].x,array[i].y);
}
return array;
}
void deletSinArray(Point * array)
{
if(array)
{
free(array);
}
}
/*
* @param sinCnt:正弦波叠加次数
* @param sampleCnt:采样点数,越多,正弦波越精细
* @param amplitude:叠加后的振幅
* @param rangeL:左边界坐标
* @param rangeR:右边界坐标
*/
Point * createSquareWave(GLint sinCnt,GLint sampleCnt,GLfloat amplitude,GLfloat rangeL,GLfloat rangeR)
{
int i = 0,j = 0;
Point * array = (Point * )calloc(sampleCnt,sizeof(Point));
for(i = 0;i<sinCnt;i++)
{
int f = 2*i+1;
/* 依次叠加正弦波,注意频域为奇数*/
Point * sinaArray = createSinArray(sampleCnt,1.0*f,1.0/f,rangeL,rangeR);
for( j = 0;j<sampleCnt;j++)
{
array[j].x = sinaArray[j].x;
array[j].y += (sinaArray[j].y*amplitude);
}
deletSinArray(sinaArray);
}
return array;
}
int g_sinCnt = 3;
GLfloat g_rangeL = -3*PI,g_rangeR = 3 * PI;
GLfloat g_amplitud = 1.0;
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
Point * squareWaveArray = createSquareWave(g_sinCnt,SAMPLE_CNT,g_amplitud,g_rangeL,g_rangeR);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE,sizeof(Point), (GLvoid *)squareWaveArray);
glEnableVertexAttribArray(0);
glDrawArrays(GL_LINE_STRIP, 0, SAMPLE_CNT);
deletSinArray(squareWaveArray);
glFlush();
}
void keyboard(unsigned char key, int x, int y)
{
switch(key)
{
case '-':
if( g_sinCnt > 1) g_sinCnt--;
break;
case '=':
case '+':
if( g_sinCnt < 50) g_sinCnt++;
break;
default:
break;
}
printf("g_sinCnt:%d g_rangeL:%f g_rangeR:%f g_amplitud:%f\n",g_sinCnt,g_rangeL,g_rangeR,g_amplitud);
glutPostRedisplay();
}
void specialKey(GLint key,GLint x,GLint y)
{
switch(key)
{
case GLUT_KEY_UP:
if( g_amplitud < 2) g_amplitud += 0.1;
break;
case GLUT_KEY_DOWN:
if( g_amplitud > 0.3) g_amplitud -= 0.1;
break;
case GLUT_KEY_LEFT:
g_rangeL -= 0.1;g_rangeR -= 0.1;
break;
case GLUT_KEY_RIGHT:
g_rangeL += 0.1;g_rangeR += 0.1;
break;
default:
break;
}
printf("g_sinCnt:%d g_rangeL:%f g_rangeR:%f g_amplitud:%f\n",g_sinCnt,g_rangeL,g_rangeR,g_amplitud);
glutPostRedisplay();
}
int main(int argc, char * argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutInitWindowPosition(200, 200);
glutInitWindowSize(300,300);
glutCreateWindow("article3");
glewInit();
init();
glutDisplayFunc(display);
glutKeyboardFunc(keyboard);
glutSpecialFunc(specialKey);
glutMainLoop();
return 0;
}
大家可以按方向键调整位置和振幅,通过+-键控制叠加次数。
叠加次数4次效果:
叠加次数50次效果:
方波波形不完善的原因应该与采样精度不足、叠加次数不够及单浮点精度不足有关。
3. uniform变量
以上程序问题大家应该看到了,效率太低,根本是用CPU来画正弦函数,计算基本在CPU上完成的,GPU的浮点计算与并行计算的优势完全没有发挥出来,有什么办法可以把浮点运算挪到GPU里去算呢?这个实现不复杂,有一个问题需先解决,除了向GPU传递顶点坐标外,我们还需要向GPU传递控制信息,如上面例子的方向键与+-的控制信息,这个可以通过uniform变量实现。
uniform变量使用流程如下:
1. shader程序定义uniform变量
2. 客户程序使用glGetUniformLocation函数获取uniform变量的索引(句柄、描述符)
3. 使用 glUniform**(程序里用 glUniform1f与 glUniform1i就可以了)把uniform变量值传送到shader程序,shader就可以用这个值了。
不同类型的glUniform**函数参数差异还挺大的,都不用记,可以直接查API用法:
https://www.khronos.org/registry/OpenGL-Refpages/gl4/
注意:uniform与in的区别需要弄清楚,红宝书2.3.2节说的比较详细。uniform 特点是所有shader都可以使用,且在shader里不可修改,in重数据传递,uniform 主要用于控制传递。
#include <GL/glew.h>
#include <GL/glut.h>
static const GLchar * vertex_source =
"#version 330 core\n"
"uniform float translate_x;\n"
"uniform float translate_y;\n"
"layout (location = 0) in vec2 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x+translate_x,position.y+translate_y,0.0,1.0);\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()
{
GLfloat translate_x_index,translate_y_index;
GLuint program = glCreateProgram();
loadShader(program,GL_VERTEX_SHADER,vertex_source);
glLinkProgram(program);
glUseProgram(program);
/* 获取uniform变量索引(位置),注意名称要和shader中的保持一致*/
translate_x_index = glGetUniformLocation(program, "translate_x");
translate_y_index = glGetUniformLocation(program, "translate_y");
/* 通过索引把信息传到GPU,供shader使用*/
glUniform1f(translate_x_index,-0.6);
glUniform1f(translate_y_index,0.3);
glClearColor(0.5f,0.5f, 1.0f, 1.0f);
}
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
GLfloat vertices[] = {0.0, 0.0,0.5,0.5,0.5,0.0};
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;
}
可以看到相比第二章的例子,shader
里增加了两个
uniform
变量,
init
时先通过变量名获取
uniform
变量位置(一般叫位置,有的也称为索引),再通过位置
把数据传送给
GPU
,整体过程还是挺简单的,大家可以改
glUniform1f
传的值看效果,效果如下:
glUniform*函数用来更新uniform变量 ,原型有很多,总的来说函数原型分成下3大类
1. 变量操作: glUniform{1|2|3|4}{f|i|d|ui}(GLint location,TYPE value);
2. 数组操作:glUniform{1|2|3|4}{f|i|d|ui}v(GLint location,GLsizei count,const TYPE * value);
3. 矩阵数组操作: glUniformFloatMatrix{2|3|4}fv(GLint location,GLsizei count,GLboolean transpose,const TYPE * value);
其中f=float,i = int,d=double,ui=unsigned int ,TYPE为对应的类型,以v结尾的都是数组类型。下面列的都是4维的函数原型:
void glUniform4i(GLint location,GLint v0,GLint v1,GLint v2,GLint v3);
void glUniform4f(GLint location,GLfloat v0,GLfloat v1,GLfloat v2,GLfloat v3);
void glUniform4iv(GLint location,GLsizei count,const GLint *value);
void glUniform4fv(GLint location,GLsizei count,const GLfloat *value);
void glUniformMatrix4fv(GLint location,GLsizei count,GLboolean transpose,const GLfloat *value);
矩阵操作只提供了数组方式的操作,矩阵方式有参数transpose参数,为true时,以行主序方式读入(c语言数组方式),false为列主序方式读取(shader中,矩阵默认以列为主序,自己组矩阵的时候需注意)。
Uniform变量数组的操作例子如下:
mat4 model_matrix[8] = {...};
glUniformMatrix4fv(render_model_matrix_loc, 8, GL_FALSE, model_matrix[0]);
注意:count的值需与矩阵数组一致,否则传送的数据不对,红宝书第三章例子ch03_drawcommands.cpp中,count的值(应为1,程序为4)不正确导致程序一片黑。
model_matrix = vmath::translation(-3.0f, 0.0f, -5.0f);
glUniformMatrix4fv(render_model_matrix_loc, 4, GL_FALSE, model_matrix);
glDrawArrays(GL_TRIANGLES, 0, 3);
4. 正弦波叠加的GLSL实现
通过uniform变量传送控制信息到shader中,就可以使用shader来实现本章的正弦叠加的例子。对于复杂的例子,使用shader文件的方式更好阅读,为方便大家编译,本章的例子先用notepad++写好,再复制上来的,阅读的时候,大家可以把分号去掉,比较方便。
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <GL/glew.h>
#include <GL/glut.h>
#define PI 3.14159265
#define SAMPLE_CNT 200
static const GLchar * vertex_source =
"#version 330 core \n"
"layout (location = 0) in float vertexSerial; \n"
"uniform int g_sinCnt; \n"
"uniform float g_rangeL; \n"
"uniform float g_rangeR; \n"
"uniform float g_amplitud; \n"
"const int sampleCnt=200; \n"
" \n"
"vec2 createSinPostion(float posIdex,float factor,float amplitude, float rangeL, float rangeR) \n"
"{ \n"
" vec2 sinPos; \n"
" float range = rangeR - rangeL; \n"
" \n"
" sinPos.x = (2.0 * posIdex - sampleCnt)/sampleCnt; \n"
" sinPos.y = amplitude * sin(factor * (rangeL + posIdex * range / sampleCnt)); \n"
" \n"
" return sinPos; \n"
"} \n"
" \n"
"vec2 createSquareWave(float posIdex,int sinCnt, float amplitude, float rangeL, float rangeR) \n"
"{ \n"
" vec2 SquareWarvePos, sinPos; \n"
" int i = 0; \n"
" \n"
" for(i = 0;i<sinCnt;i++) \n"
" { \n"
" int f = 2 * i + 1; \n"
" \n"
" sinPos = createSinPostion(posIdex, 1.0 * f, 1.0 / f, rangeL, rangeR); \n"
" SquareWarvePos.x = sinPos.x; \n"
" SquareWarvePos.y += (sinPos.y * amplitude); \n"
" } \n"
" \n"
" return SquareWarvePos; \n"
"} \n"
" \n"
"void main() \n"
"{ \n"
" vec2 SquareWarvePos = createSquareWave(vertexSerial,g_sinCnt,g_amplitud,g_rangeL,g_rangeR); \n"
" gl_Position = vec4(SquareWarvePos,0.0,1.0); \n"
"} \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);
}
/* uniform控制变量的位置定义*/
GLint sinCntIdx;
GLfloat rangeLIdx,rangeRIdx,amplitudIdx;
/* uniform控制变量的值定义*/
GLint g_sinCnt = 3;
GLfloat g_rangeL = -3 * PI,g_rangeR = 3 * PI,g_amplitud = 1.0;
void init()
{
GLuint program = glCreateProgram();
loadShader(program, GL_VERTEX_SHADER, vertex_source);
glLinkProgram(program);
glUseProgram(program);
/* 获取shader中uniform变量位置*/
sinCntIdx = glGetUniformLocation(program, "g_sinCnt");
rangeLIdx = glGetUniformLocation(program, "g_rangeL");
rangeRIdx = glGetUniformLocation(program, "g_rangeR");
amplitudIdx = glGetUniformLocation(program, "g_amplitud");
glClearColor(0.5f, 0.5f, 1.0f, 1.0f);
}
void display()
{
glClear(GL_COLOR_BUFFER_BIT);
/* 向shader中uniform变量传送值*/
glUniform1i(sinCntIdx,g_sinCnt);
glUniform1f(rangeLIdx,g_rangeL);
glUniform1f(rangeRIdx,g_rangeR);
glUniform1f(amplitudIdx,g_amplitud);
/* 顶点的坐标由shader自己生成,但需告知shader点的索引*/
GLfloat vertexSerial[SAMPLE_CNT];
for(int i = 0;i<SAMPLE_CNT;i++)
{
vertexSerial[i] = i;
}
glVertexAttribPointer(0, 1, GL_FLOAT, GL_FALSE, sizeof(GLfloat), (GLvoid *)vertexSerial);
glEnableVertexAttribArray(0);
glDrawArrays(GL_LINE_STRIP, 0, SAMPLE_CNT);
glFlush();
}
void keyboard(unsigned char key, int x, int y)
{
switch (key)
{
case '-':
if (g_sinCnt > 1) g_sinCnt--;
break;
case '=':
case '+':
if (g_sinCnt < 50) g_sinCnt++;
break;
default:
break;
}
printf("g_sinCnt:%d g_rangeL:%f g_rangeR:%f g_amplitud:%f\n", g_sinCnt, g_rangeL, g_rangeR, g_amplitud);
glutPostRedisplay();
}
void specialKey(GLint key, GLint x, GLint y)
{
switch (key)
{
case GLUT_KEY_UP:
if (g_amplitud < 2) g_amplitud += 0.1;
break;
case GLUT_KEY_DOWN:
if (g_amplitud > 0.3) g_amplitud -= 0.1;
break;
case GLUT_KEY_LEFT:
g_rangeL -= 0.1;
g_rangeR -= 0.1;
break;
case GLUT_KEY_RIGHT:
g_rangeL += 0.1;
g_rangeR += 0.1;
break;
default:
break;
}
printf("g_sinCnt:%d g_rangeL:%f g_rangeR:%f g_amplitud:%f\n", g_sinCnt, g_rangeL, g_rangeR, g_amplitud);
glutPostRedisplay();
}
int main(int argc, char * argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGBA);
glutInitWindowPosition(200, 200);
glutInitWindowSize(300, 300);
glutCreateWindow("article3");
glewInit();
init();
glutDisplayFunc(display);
glutKeyboardFunc(keyboard);
glutSpecialFunc(specialKey);
glutMainLoop();
return 0;
}
运行的结果(效果)与客户端实现是一致的,效率上应该会更高一些,这里就不贴截图了。
大家可以看到使用shader语言来实现正弦叠加函数,shader的编写与客户端代码编写最大的不同是shader处理的是单个点坐标, 程序中去掉了采样点循环,程序更简洁,语法和C语言基本一致,客户端代码移植到shader中去,只需做简单的修改即可。
客户度向GPU只传送了采样点的序列,传送的数据小很多,如果使用vbo与vao,display传送数据部分,可以进一步简化和提高效率。
注意:
1. 目前调试shader程序没什么好的办法,红宝书shader的例子都很简短,调试长一点的shader,请大家先把loadShader里的错误处理加上。
2. GLint,GLfloat等是openGL客户端的变量类型,shader里面用的都是int,float等,与c类似的未封装类型。
3. GLSL内置了很多函数,上述的sin函数就是内置的,内置函数原型用的时候可以查手册(红宝书附录C),避免与c、c++的库函数搞混了。