openGL之glsl入門3--正弦函式疊加為方波
使用GLSL畫點,畫線,畫面,與原來使用glBegin(), glEnd()的方式有類似的地方,原來方式api比較多,GLSL採用的陣列一次傳送,程式的本質還是座標點的設計與確認,只要知道圖怎麼畫,哪種方式差異不大,本章主要介紹:
1. 正弦函式的基本畫法
2. 鍵盤的控制
3. uniform變數的用法
4. 正弦波疊加為方波的GLSL實現
GLSL畫這些基本的型別是,使用的函式主要是glDraw*系列的函式,這裡再說一下:
void glDrawArrays (GLenum mode, GLint first, GLsizei count);
mode與老的方式一致,有以下型別,
點面的畫法的例子比較多,請大家自己去練(可以找老的程式改用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用法:
注意: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++的庫函式搞混了。