opengl学习总结

opengl流程

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口确实2D像素组,这导致OpenGL大部分的工作都是关于把3D坐标转变为适应屏幕的2D像素;3D转成2D坐标的处理过程是OpenGl的图形渲染管线(Graphic Pipeline,大多译为管线,实际上指的是一堆原始图形数据途径一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的;图形渲染管线可以划分为两个主要部分,第一部分把你3D的坐标转换成2D坐标;第二部分是把2D坐标转变成实际的有颜色的像素;

1
2D坐标和像素不同,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到屏幕/窗口分辨率的限制

openGL首先接收用户提供的几何数据(顶点和几何图元),并且将它输入到一系列着色器阶段中进行处理,包括:顶点着色(vertex shader ),细分着色,以及最后的几何着色,然后被送入光栅化单元(resterizer)光栅化单元负责对所有剪切区域(clipping region)内的图元生成片元数据,然后对每个生成的片元都执行一个片元着色器;

1561280730893

下面有个更清晰容易理解的图~

顶点着色器(Vertex Shader),它把一个单独的顶点(Vertex)作为输入,顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理;如:顶点变换,法向量变化和单位化,生成相应的纹理坐标,纹理坐标变换,雾坐标,光照计算; 当需要计算顶点在屏幕上的位置,就会涉及到矩阵变换

图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(r如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定的图形,如上图就是一个三角形;

几何着色器(Geometry Shader)几何图形把图元形式的一系列顶点的集合作为输入,它可以通过产生新的顶点构造出新的图元来生成其他形状;这个着色阶段是可选

之后就会传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment).在这之前还会线做下裁切(Cliping)会丢弃超出视图以外的所有像素,用来提升效率;

片段着色器的主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照,阴影,光的颜色等等),这些数据可以用来计算最终像素的颜色;片元着色器的作用有:雾;提取纹理单元,用于纹理贴图;颜色混合,等

最后的测试和混合简单而言是根据Zorder和Alpha值进行的图形合成过程;

先有个大概流程的印像,接下来我们会通过一些例子慢慢将流程梳理清楚;先从opengl的基本库开始讲起;

Opengl相关库

opengl基本函数库用来描述图元(graphics output promitive),属性(attribute),几何变换(geometric transformation),观察变换(viewing transform)和进行其他的操作;由于OpenGl被设计成硬件无关型,因此输入和输出函数等许多操作均不包括在其基础库中;而是放在OpenGl开发的辅助库中;

Opengl基本库(也称OpenGL核心库):常见有gl开头的这些函数的写法glBegin, glClear, glCopyPixels, glPolygonMode;变量中GL_2D,GL_RGB,GL_CCW;GL_POLYGON,GL_AMBIENT_AND_DIFFUSE;

相关库:OpenGL实用函数(openGL Utility,GLU)提供了一些例程,可以设置观察和投影矩阵,利用线条和多边形近似法来描述复杂对象,使用线性近似法显示二次曲线和样条曲线,处理表面绘制操作,以及完成其他复杂任务。每一个OpenGL实现中都包括了GLU库,所有的函数名都用glu开头。窗口显示系统,opengl实用函数工具包(OpenGL Utility Toolkit,GLUT)提供了与任意屏幕窗口系统进行交互的函数库GLUT库函数以glut为前缀,该库也包含了描述与绘制二次和样条曲线及曲面的方法;

说这么多,先来个demo~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <GL/GLUT.h>
void init(void)
{
glClearColor(1.0, 1.0, 1.0, 1.0);//使用RGB颜色值将显示窗口的背景颜色设定为白色,参数解析前面为RGB,最后为alpha值

glMatrixMode(GL_PROJECTION);//设置投影矩阵,
gluOrtho2D(0.0, 200.0, 0.0, 150.0);//正交投影见过世界坐标系二维矩阵区域的内容映射到屏幕上,区域的x的坐标值从0.0到200,y坐标从0到150.只要再该矩形内定义的对象,都会出现在显示窗口
}

void lineSegment(void)
{
glClear(GL_COLOR_BUFFER_BIT);//用来指定它是颜色缓存中的位值

glColor3f(0.0, 0.4, 0.2);//设置线段的颜色
glBegin(GL_LINES);
glVertex2i(180, 15);
glVertex2i(10, 145);
glEnd();

glFlush();

}

void main(int argc, char** argv)
{
glutInit(&argc, argv); //初始化glut.该函数也能处理命令行参数
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); //指定显示窗口使用单个缓存和用RGB的颜色模型旋转颜色
glutInitWindowPosition(50, 100);//窗口显示的地方在在显示器的左上角向右50,向下100像素开始显示
glutInitWindowSize(400, 300);//设定显示窗口的初始宽度和高度的像素数
glutCreateWindow("An Example"); //窗口创建的时候给个标题
init();
glutDisplayFunc(lineSegment);//显示lineSegment线段
glutMainLoop();//激活已创建的窗口和图形显示的内容
}

显示效果如下:

demo

通过这个demo我们大致了解到opengl的大致使用步骤

  1. 初始化物体渲染所对应的状态。
  2. 设置需要渲染的物体

接着demo下面我们引入几个概念:

MVP

demo中的init函数中glMatrixMode(GL_PROJECTION);//设置投影矩阵,这个涉及到了投影矩阵,对于opengl常见的就是要进行矩阵变换;我们这里稍微带下

矩阵变换

  1. 红色坐标系位模型坐标系,指定了模型各个点的位置,每个点是(x,y,z)组成的数组;由于后面的矩阵都是4*4,所以在后面补上1;
  2. 模型变换,将模型坐标转化成世界坐标,把物体在世界坐标系的位置拆分成旋转,平移,缩放的表达式;
  3. 视图变换:指定一个相机的位置和角度,然后去观察世界坐标系下的物体;
  4. 投影变换:把前面三位空间的坐标系投影到二维屏幕的坐标系,除了屏幕的横纵坐标,另外一个维度就是垂直屏幕方向的坐标,就是之后可以写入深度缓冲区的值。将三维坐标转换到二维屏幕,主要分为正交投影和透视投影,都是用相似三角形算比例;
  5. 视口变换,这里只是一个非常简单的XoY平面上的缩放;它决定了最终渲染到平面的哪一块,所以用之前的缩放同样的处理就能得到相应矩阵;这里相当于我们经常在ui中遇到的缩放因子;

于是乎:

1
变换后的坐标=视口矩阵 * 投影矩阵(P) * 视图矩阵(V) * 模型矩阵(M) * 模型点坐标

对应的VR里面左右眼的矩阵模型公式:

1
leftEyeMvp = projectMatrix*projRot(投影矩阵) * mHeadOrient*mleftEye(视口矩阵) * mode(模型点)

通过这个矩阵我们就能从三维空间映射到我们的屏幕上,由于左右眼的视口矩阵不同产生的视觉差,让我们又产生了3d的效果;

这里有两个工具可以更好的理解这一概念:

MODELVIEW

http://www.songho.ca/opengl/files/matrixModelView.zip

PROJECTION

http://www.songho.ca/opengl/files/matrixProjection.zip

知道了矩阵变换后;我们还会经常遇到着色器这个名字;那这个是什么呢?

着色器

着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行;在openGL中着色器就相当于画笔,而顶点vertices相当于图形(把一个个点按顺序用线连接起来就是一个图形),着色器OpenGL分成两个部分,一个用于绘制顶点的顶点着色器VerticesShader,一个用于顶点连线后所包围的区域填充颜色的片元着色器,可以理解为windows画图中的填充工具;

1560837291736

我们常见的着色器普遍认为有三种,在移动设备上使用较多的OpenGL的GLSL,在桌面使用较多微软推的DirectX的HSLS(High level shader language);还有NVIDIA公司的CG(C for Graphic);谷歌后面会推的Vulkan,着色器语言是基于GLSL为基础进行增强;由于本文基于的是OpenGL所以就是用的GLSL;

语言总是苍白的,来段代码吧~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#include <stdio.h>
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/glut.h>


float g_lightPos[4] = {1,0.5,1,0};

void changeSize(int w, int h) {

// Prevent a divide by zero, when window is too short
// (you cant make a window of zero width).
if(h == 0)
h = 1;

float ratio = 1.0* w / h;

// Reset the coordinate system before modifying
glMatrixMode(GL_PROJECTION);
glLoadIdentity();

// Set the viewport to be the entire window
glViewport(0, 0, w, h);

// Set the correct perspective.
gluPerspective(45,ratio,1,1000);
glMatrixMode(GL_MODELVIEW);
}


void renderScene(void) {

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

glLoadIdentity();
gluLookAt(0.0,0.0,5.0,0.0,0.0,-1.0,0.0f,1.0f,0.0f);

glLightfv(GL_LIGHT0, GL_POSITION, g_lightPos);
glutSolidTeapot(1);

glutSwapBuffers();
}

void processNormalKeys(unsigned char key, int x, int y) {

if (key == 27)
exit(0);
}

char* readShaderSource(const char *fileName)
{
FILE *fp;
char *content = NULL;
int count=0;

if (fileName != NULL)
{
fp = fopen(fileName,"rt");

if (fp != NULL)
{
fseek(fp, 0, SEEK_END);
count = ftell(fp);
rewind(fp);
if (count > 0)
{
content = (char *)malloc(sizeof(char) * (count+1));
count = fread(content,sizeof(char),count,fp);
content[count] = NULL;
}
fclose(fp);
}
}
return content;
}

GLuint genShader(GLenum type,const char* fileName,char*& log)
{
//创建着色器对象
GLuint shader = glCreateShader(type);
//从文件中读取着色器的实现代码
char* shaderSource = readShaderSource(fileName);
if( !shaderSource )
return 0;
const char* ptrShaderSource = shaderSource;
//将着色器的实现代码与创建的着色器对象绑定
glShaderSource(shader,1,&ptrShaderSource,NULL);
free(shaderSource);
//编译着色器对象
glCompileShader(shader);
GLint status = 0;
//查看编译状态
glGetShaderiv(shader,GL_COMPILE_STATUS,&status);
if( !status )
{
GLint length;
//读取日志信息的长度
glGetShaderiv(shader,GL_INFO_LOG_LENGTH,&length);
log = (GLchar*)malloc(length);
//读取日志信息
glGetShaderInfoLog(shader,length,&length,log);
#if 1
printf("%s\n",log);
#endif
//删除着色器对象
glDeleteShader(shader);
return 0;
}
return shader;
}

GLuint linkProgram(GLuint* shader,int shaderNum,char*& log)
{
//创建着色器程序
GLuint program = glCreateProgram();
int i;
//往着色器程序中加入着色器对象
for( i=0 ; i<shaderNum ; i++ )
glAttachShader(program,shader[i]);
//链接着色器程序
glLinkProgram(program);
GLint status;
//查看链接状态
glGetProgramiv(program,GL_LINK_STATUS,&status);
if( !status )
{
GLint length;
//读取日志信息的长度
glGetProgramiv(program,GL_INFO_LOG_LENGTH,&length);
log = (GLchar*)malloc(length);
//读取日志信息
glGetProgramInfoLog(program,length,&length,log);
#if 1
printf("%s\n",log);
#endif
//删除着色器对象
glDeleteProgram(program);
return 0;
}
return program;
}

void useProgram(GLuint program)
{
//运行创建成功的着色器程序
glUseProgram(program);
}

bool setShaders()
{
char* log = NULL;
//创建一个顶点着色器对象
GLuint vertexShader = genShader(GL_VERTEX_SHADER,"toon.vert",log);
if( !vertexShader )
{
free(log);
return false;
}
//创建一个片断着色器对象
GLuint fragmentShader = genShader(GL_FRAGMENT_SHADER,"toon.frag",log);
if( !fragmentShader )
{
free(log);
glDeleteShader(vertexShader);
return false;
}

//把创建好的顶点和片断着色器对象链接到着色器程序中
GLuint shader[2] = {vertexShader,fragmentShader};
GLuint program = linkProgram(shader,2,log);
if( !program )
{
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
free(log);
return false;
}
//使用创建成功的着色器程序
useProgram(program);

return true;
}

int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100,100);
glutInitWindowSize(320,320);
glutCreateWindow("Shader-Demo");

glutDisplayFunc(renderScene);
glutReshapeFunc(changeSize);
glutKeyboardFunc(processNormalKeys);

glEnable(GL_DEPTH_TEST);
glClearColor(1.0,1.0,1.0,1.0);


glewInit();
//判断是否支持GLSL
if (GLEW_ARB_vertex_shader && GLEW_ARB_fragment_shader)
printf("Ready for GLSL\n");
else
{
printf("No GLSL support\n");
exit(1);
}

setShaders();
glutMainLoop();

return 0;
}

顶点着色器

1
2
3
4
5
6
7
8
9
varying vec3 normal, lightDir;

void main()
{
lightDir = normalize(vec3(gl_LightSource[0].position));
normal = normalize(gl_NormalMatrix * gl_Normal);

gl_Position = ftransform();
}

当我们讨论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的

片源着色器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

varying vec3 normal, lightDir;

void main()
{
float intensity;
vec3 n;
vec4 color;

n = normalize(normal);
intensity = max(dot(lightDir,n),0.0);

if (intensity > 0.98)
color = vec4(0.8,0.8,0.8,1.0);
else if (intensity > 0.5)
color = vec4(0.4,0.4,0.8,1.0);
else if (intensity > 0.25)
color = vec4(0.2,0.2,0.4,1.0);
else
color = vec4(0.1,0.1,0.1,1.0);

gl_FragColor = color;
}

效果图如下所示:

1561355152918

总结

着色器的创建流程 genShader

  1. 创建一个着色器对象 glCreateShader
  2. 将着色器的实现代码与创建的着色器对象绑定 glShaderSource;
  3. 把着色器源代码编译为目标代码 glCompileShader
  4. 验证是否编译通过 glGetShaderiv

接着,把创建好的着色器加入到着色程序中

  1. 创建一个着色程序 glCreateProgram;
  2. 把适当的着色对象连接到这个着色程序中 glAttachShader
  3. 链接到这个着色器程序 glLinkProgram
  4. 验证这着色器结点已经成功完成 glGetProgramiv
  5. 使用着色器进行顶点或者片元处理 useProgram

对于安卓来讲,会把这些接口都进行封装,简化使用;

参考资料

搞懂矩阵运算

第三课:矩阵

着色器

openGL ES学习之着色器语言

Android OpenGL ES从白痴到入门

openGl 矩阵变换

openGL着色器介绍

OpenGL替代者-Vulkan

是立体还是平面