OpenGL学习之路(四)
1 引子
上次读书笔记主要是学习了应用三维坐标变换矩阵对二维的图形进行变换,并附带介绍了GLSL语言的编译、链接相关的知识,之后介绍了GLSL中变量的修饰符,着重介绍了uniform修饰符,来向着色器程序传入输入参数。
这次读书笔记的内容相对有趣一些,主要是和园友们分享讨论三维坐标变换矩阵在三维几何体上的应用,以及介绍一下如何实现三维图形与用户操作的交互。这一次笔记在三维编程中也是非常重要的——我们最后开发的三维程序最终就是要和目标用户进行交互的。
之前一直没有在博客上放过gif格式的动画图片,这次因为涉及交互操作,所以想在博文上贴几张gif格式的动画图片。为此,我得找一款好的录屏软件和格式转换软件,结果找了半天没找到使用比较方便且制作出来效果比较好的软件,还下到了流氓软件,不免吐槽下——找PC好软件还是得多长几个心眼啊,最好是上官网去下载!最后找到了一个直接可以录屏并生成gif,且支持编辑,在此推荐给大家——抠抠视频秀。不过,最后生成的动画图片比较大,考虑到在手机上逛园子童鞋,博主就不上传了,太多图片还是很费流量的,我把代码和可执行文件传到网盘上了,需要的童鞋可以自行去下载。哎,瞎忙乎了一场~~~~(>_<)~~~~。
好,谈正事,让我们继续踏上OpenGL学习之路——第四站。
2 三维变换应用
2.1 场景准备
既然是学习三维坐标的变换,总得有一个变为对象——三维场景(几何体)。在读书笔记(三)中我们用的是正方形——一个简单得不能再简单的正方形。这一次仍然使用一个最简单的三维图形——立方体。首先还是让我们来看看例子程序代码,其实和之前讲的几乎没什么区别,也就是那么几步:开辟缓冲区、上传顶点数据、设置顶点属性,最后渲染图形,具体程序代码如下:
1 #include <iostream>
2
3 #include "GameFramework/StdAfx.h"
4 #include "Resource/GPUProgram.h"
5 #include "AlgebraicEntity/Matrix.h"
6
7 GPUProgram program;
8
9 void initialize_04()
10 {
11 // --------------准备立方体顶点数据--------------
12 // 0/----------------/1
13 // /| /|
14 // / | / |
15 //4/--|-------------/5 |
16 // | | | |
17 // | | | |
18 // | | | |
19 // | 3/-------------|--/2
20 // | / | /
21 // |/ |/
22 //7/----------------/6
23 GLfloat cube_vertices[8][4] = {
24 { -0.5f, 0.5f, -0.5f, 1.0f },
25 { 0.5f, 0.5f, -0.5f, 1.0f },
26 { 0.5f, -0.5f, -0.5f, 1.0f },
27 { -0.5f, -0.5f, -0.5f, 1.0f },
28 { -0.5f, 0.5f, 0.5f, 1.0f },
29 { 0.5f, 0.5f, 0.5f, 1.0f },
30 { 0.5f, -0.5f, 0.5f, 1.0f },
31 { -0.5f, -0.5f, 0.5f, 1.0f }
32 };
33
34 // --------------准备顶点颜色数据--------------
35 GLfloat cube_colors[8][4] = {
36 { 1.0f, 1.0f, 1.0f, 1.0f },
37 { 1.0f, 1.0f, 0.0f, 1.0f },
38 { 1.0f, 0.0f, 1.0f, 1.0f },
39 { 0.0f, 1.0f, 1.0f, 1.0f },
40 { 0.0f, 0.0f, 1.0f, 1.0f },
41 { 0.0f, 1.0f, 0.0f, 1.0f },
42 { 1.0f, 0.0f, 0.0f, 1.0f },
43 { 0.0f, 0.0f, 0.0f, 1.0f }
44 };
45
46 // --------------创建缓冲区对象,分配空间,上传数据--------------
47 GLuint buffer_ID;
48 glGenBuffers(1, &buffer_ID);
49 glBindBuffer(GL_ARRAY_BUFFER, buffer_ID);
50 glBufferData(GL_ARRAY_BUFFER,
51 sizeof(cube_vertices) + sizeof(cube_colors), NULL, GL_STATIC_DRAW);
52 glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(cube_vertices), cube_vertices);
53 glBufferSubData(GL_ARRAY_BUFFER, sizeof(cube_vertices), sizeof(cube_colors), cube_colors);
54
55 // --------------创建并设置顶点属性对象--------------
56 GLuint VAO_ID;
57 glGenVertexArrays(1, &VAO_ID);
58 glBindVertexArray(VAO_ID);
59 glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
60 glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(sizeof(cube_vertices)));
61 glEnableVertexAttribArray(0);
62 glEnableVertexAttribArray(1);
63
64 // --------------准备顶点索引数据--------------
65 GLushort vertex_indices[] = {
66 1, 5, 0, 4, 3, 7, 2, 6,
67 7, 4, 6, 5, 1, 2, 0, 3
68 };
69 GLuint EBO_ID;
70 glGenBuffers(1, &EBO_ID);
71 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO_ID);
72 glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(vertex_indices), vertex_indices, GL_STATIC_DRAW);
73
74 program.AddShader(GL_VERTEX_SHADER, "F:/VC++游戏编程/OpenGLGuide/OpenGL05/cube.vert");
75 program.AddShader(GL_FRAGMENT_SHADER, "F:/VC++游戏编程/OpenGLGuide/OpenGL05/cube.frag");
76 glUseProgram(program.CreateGPUProgram());
77 }
78
79 void display_04()
80 {
81 glClear(GL_COLOR_BUFFER_BIT);
82
83 glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, BUFFER_OFFSET(0));
84 glDrawElements(GL_TRIANGLE_STRIP, 8, GL_UNSIGNED_SHORT, BUFFER_OFFSET(8 * sizeof(GLushort)));
85 }
86
87 int main(int argc, char **argv)
88 {
89 glutInit(&argc, argv);
90 glutInitDisplayMode(GLUT_RGBA);
91 glutInitWindowSize(512, 512);
92 glutInitContextVersion(3, 3);
93 glutInitContextProfile(GLUT_CORE_PROFILE);
94 glutCreateWindow("学习之路(四)");
95
96 glewExperimental = TRUE;
97 if (glewInit())
98 {
99 std::cerr << "Unable to initialize GLEW... Exiting..." << std::endl;
100 std::exit(EXIT_FAILURE);
101 }
102
103 initialize_04();
104 glutDisplayFunc(display_04);
105
106 glutMainLoop();
107 return 0;
108 }
可以看出:程序的整体框架与之前是类似的,由初始化函数(initialize_04)和绘制函数(display_04)构成。其中初始化函数负责数据的准备和输入给OpenGL,这里的数据包括立方体顶点数据(这里给出的是齐次坐标)、顶点颜色数据以及顶点索引数据(在顶点数组中的索引位置,从0开始)。在我们的示例中,数据准备是比较简单的,但在实际项目开发时,数据的准备通常会很复杂。绘制函数则调用OpenGL绘制相关的API来实现几何体的绘制,这里我们将三角形拆开为两个三角形条带(见下图),这样绘制立方体的过程就是绘制两个三角形带(triangle strip)。
当然,为了绘制,还需要加入顶点着色器和片元着色器。这两个着色器都很简单,其中顶点着色器是从传递输入顶点数据到GLSL的内置变量gl_Position中,并将顶点颜色数据直接输出,由OpenGL进行插值,然后传到片元着色器;片元着色器得到光栅化(插值)之后的颜色值,直接输出即可,两个着色程序代码如下:
顶点着色程序:
1 #version 330 core
2
3 layout(location = 0) in vec4 vertex_position;
4 layout(location = 1) in vec4 vertex_color;
5
6 out vec4 temp_color;
7
8 void main()
9 {
10 gl_Position = vertex_position;
11 temp_color = vertex_color;
12 }
片元着色程序:
1 #version 330 core
2
3 in vec4 temp_color;
4
5 out vec4 out_color;
6
7 void main()
8 {
9 out_color = temp_color;
10 }
下图便是上述程序的执行结果,看上去像一个正方形,但其实它是立方体,稍后我们对它做了旋转操作之后就可以看到庐山真面目了。
看完了绘制结果,让我们继续学习上述程序中用到的新的OpenGL API。第一个新的API就是往缓存中分段上传数据,例子中我们往缓存中分别上传了顶点坐标数据和顶点颜色数据。我们之前用glBufferData来分配并上传数据,但该函数只能一次性输入所有数据,如果客户端的数据保存在不同数据结构中,但想上传到同一个目标缓冲区对象时,就得使用下面这个新的API了:
void glBufferSubData(GLenum target offset, GLintptr offset, GLsizeiptr size, const GLvoid *data) 函数功能:将data指针所指向的、大小为size个字节的数据上传到当前绑定的缓冲区对象所管理的、偏移量为offset的内存区域。
target ——目标缓冲区对象类型
offset ——缓冲区对象所管理内存的偏移量
size ——上传数据的字节数
data ——指向数据的指针
用示意图更形象的表达如下:
通过上述接口,就可以将顶点坐标数据、顶点的其它属性(如颜色)一些数据分开存放,分段上传至缓冲区对象管理的显存中。
之前博文中,我们都是使用glDrawArray来绘制几何体的。这个绘制API的顶点数据是从绑定到目标为GL_ARRAY_BUFFER缓冲区对象得到。除此之外,OpenGL还支持通过顶点的索引值来间接得到顶点数据来绘制几何体,这样做的好处很显然的——避免顶点数组中的重复项。所使用的OpenGL绘制API为glDrawElements(),使用这种绘制方式,除了需要提供顶点数组数据外,还需要需要告诉OpenGL,绘制时所使用的顶点索引数组,这时就要使用另一个绑定目标——GL_ELEMENT_ARRAY_BUFFER(见程序71~72行)——的缓冲区对象了,具体使用方法与绑定目标为GL_ARRAY_BUFFER的缓冲区对象是一模一样的。好了,到此为止,初始化部分已经完成了,个人感觉有了读书笔记(一)作为基础,这里理解起来就很简单了。下面,来看看刚才提到的、使用索引来绘制几何体的API,其函数签名为:
void glDrawElement(GLenum mode, GLsizei count, GLenum type, const GLvoid *indices) 函数功能:使用count个索引值来绘制一个mode类型的图元,索引值类型为type,索引值数据保存在GL_ELEMENT_ARRAY_BUFFER绑定的缓冲区对象中,绘制使用的索引数据为偏移量indices开始的count个索引值 mode ——绘制图元的类型
count ——用到的索引值个数
type ——索引值的类型
indices ——索引值在缓冲区所管理内存中的偏移量
这个命令的功能和glDrawArray一样,区别在于所获取顶点数据的方式不同:glDrawArray直接使用顶点数组来绘制图元;glDrawElements则使用顶点索引值来间接获取绘制图元的顶点来绘制几何体。
2.2 变换矩阵的使用
上一小节绘制出来的立方体是静止不动的,那么如何让它动起来呢?这就需要将变换矩阵应用到几何图像的顶点数据上,为此要修改源程序。首先,修改顶点着色器,在顶点着色器中将顶点数据传递给内置变量gl_Position变量前,对它做一次矩阵变换,改变之后的着色器程序如下(改动部分已由红色字符标出):
1 #version 330 core
2
3 uniform mat4 mat_transform;
4
5 layout(location = 0) in vec4 vertex_position;
6 layout(location = 1) in vec4 vertex_color;
7
8 out vec4 temp_color;
9
10 void main()
11 {
12 gl_Position = mat_transform * vertex_position;
13 temp_color = vertex_color;
14 }
着色器程序中用到的矩阵数据要由客户端输入,本文中立方体体的变换方式由用户决定,客户端修改包括两部分:
1. 这需要在main函数中向GLUT注册接收键盘输入回调函数来接收用户的输入,如下(红色标出部分):
1 int main(int argc, char **argv)
2 {
3 glutInit(&argc, argv);
4 glutInitDisplayMode(GLUT_RGBA);
5 glutInitWindowSize(512, 512);
6 glutInitContextVersion(3, 3);
7 glutInitContextProfile(GLUT_CORE_PROFILE);
8 glutCreateWindow("学习之路(四)");
9
10 glewExperimental = TRUE;
11 if (glewInit())
12 {
13 std::cerr << "Unable to initialize GLEW... Exiting..." << std::endl;
14 std::exit(EXIT_FAILURE);
15 }
16
17 initialize_04();
18 glutDisplayFunc(display_04);
19 glutKeyboardFunc(display_keydown);
20
21 glutMainLoop();
22 return 0;
23 }
2. 定义键盘输入回调函数display_keydown,如下:
1 void display_keydown(unsigned char key, int x, int y)
2 {
3 // --------------准备矩阵数据--------------
4 Matrix4X4 mat;
5 if (key == 'x') // 绕x轴顺时针旋转
6 {
7 mat = Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(1.0, 0.0, 0.0));
8 }
9 else if (key == 'y') // 绕y轴顺时针旋转
10 {
11 mat = Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(0.0, 1.0, 0.0));
12 }
13 else if (key == 'z') // 绕z轴顺时针旋转
14 {
15 mat = Matrix4X4::CreateRotateMatrix(PI / 12, Vector3D(0.0, 0.0, 1.0));
16 }
17 else if (key == '+') // 放大
18 {
19 mat = Matrix4X4::CreateScaleMatrix(1.1);
20 }
21 else if (key == '-') // 缩小
22 {
23 mat = Matrix4X4::CreateScaleMatrix(0.9);
24 }
25 else if (key == 'l') // 左平移
26 {
27 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(-0.1, 0.0, 0.0));
28 }
29 else if (key == 'r') // 右平移
30 {
31 mat = Matrix4X4::CreateTranslateMatrix(Vector3D(0.1, 0.0, 0.0));
32 }
33 mat_transform = mat * mat_transform;
34
35 // --------------上传矩阵数据--------------
36 glUniformMatrix4fv(mat_location, 1, GL_TRUE, mat_transform._m);
37
38 // --------------绘制图像--------------
39 glutPostRedisplay();
40 }
各输入字符所代表的含义在代码注释中已经详细说明了,在此不再赘述。回调函数中,根据不同的用户输入,生成读书笔记(二)中介绍的旋转、平移、缩放矩阵;然后让它与原有的变换矩阵(保存在全局变量mat_transform中)相乘,需要注意的是:新的变换矩阵一定要放在乘号(*)的左边——矩阵乘法不具有交换性;通过函数glUniformMatrix4fv向OpenGL上传新的复合变换矩阵,最后调用glutPostRedisplay()函数重新绘制图像即可。另外,在初始化函数initialize_04中也需要传入单位矩阵作为初始的变换矩阵,否则一开始会没有图像。运行程序,通过键盘输入回调函数中给出的字符,可以得到下图所示的运行结果:
3 鼠标驱动操作
第二节中的操作均由键盘输入的,但在实际使用中,用户常常是用鼠标来操作。下面我们来瞅瞅,如何通过鼠标怎么驱动物体的平移、旋转和缩放的。
3.1 鼠标驱动平移
平移操作可以说是最简单的了,起始点和终止点相当于确定了平移向量。不过鼠标给出的是像素坐标,在求平移向量之前需要将像素坐标转换为世界坐标系,转换公式如下:
特别注意的是,屏幕像素坐标系xx轴与OpenGL的世界坐标系下的xx轴是同向的,而yy轴恰好相反——是反响的,所以上述变换公式从形式上看,xx和yy相差一个负号,将其封装为一个辅助函数,如下:
1 Point3D Point2DHelper::ConvertPointFromScreenToWorld( const Point2D& pt2D )
2 {
3 double dWorldX = 2 * pt2D.x / m_iScreenWidth - 1.0;
4 double dWorldY = 1.0 - 2 * pt2D.y / m_iScreenHeight;
5 double dWorldZ = 0.0;
6 return Point3D(dWorldX, dWorldY, dWorldZ);
7 }
这里,m_iScreenWidth和m_iScreenHeight分别为窗口的宽和高。得到起始点和终止点的坐标后,根据读书笔记(二)中所讲的知识点就可以求得平移变换矩阵。客户端的代码将在3.4节与缩放、旋转变换一并给出。
3.2 鼠标驱动缩放
鼠标驱动缩放形式有许多种——只要将向量映射为一个数即可,这里我们采用的策略是住右下方向拖拽是缩小,往左上方向拖拽为放大,得到如下映射变换公式:
得到缩放系数之后,根据读书笔记(二)所介绍的知识便可求得缩放矩阵,客户端代码将在3.4节给出。
3.3 鼠标驱动旋转
刚才主要阐述了通过鼠标实现物体的平移、旋转。平移只要得到平移向量就可以了,缩放只要得到缩放系数即可,但对于旋转,则没有那么简单了,下面就来具体学习一下吧!通过鼠标来驱动物体的旋转,本质就是通过屏幕上的两个点,确定旋转角度和旋转轴,确定这两个参数的算法称为ArcBall算法。此算法可以用下面示意图来分析:
已知:黑色的二维坐标系OxyOxy是屏幕坐标系,橙色的三维坐标系OxyzOxyz是OpenGL的世界坐标系;SS点是鼠标的起始点,EE点为鼠标的终止点;确定S到E的旋转矩阵。ArcBall算法是按下面步骤执行的:
第一步:将屏幕看成一个"z>0z>0"的半球面(这样就能旋转了嘛!),将鼠标起点SS和终点EE往半球面上投影,投影方法很简单,xx和yy保持不变,zz按下述规则求出:
第二步:求旋转轴,也很简单,即向量OS′→OS′→和向量OE′→OE′→张成的平面的法向量,即:
第三步:求旋转角度,也不难,即向量OS′→OS′→和向量OE′→OE′→的夹角,即:
第四步:根据旋转轴和旋转角度,求得旋转矩阵(具体方法见读书笔记(二))。
将上述步骤封装在ArcBall类中,具体程序代码如下:
1 Matrix4X4 ArcBall::CreateRotateMatrix(
2 const Point3D& ptPlaneStart, const Point3D& ptPlaneEnd )
3 {
4 // ----------------相等,直接返回单位矩阵----------------
5 if (ptPlaneStart == ptPlaneEnd)
6 {
7 Matrix4X4 mat;
8 return mat;
9 }
10
11 // ----------------投影至半球----------------
12 Point3D ptSphereStart = ProjectPointToSemiSphere_i(ptPlaneStart);
13 Point3D ptSphereEnd = ProjectPointToSemiSphere_i(ptPlaneEnd);
14
15 // ----------------求旋转轴----------------
16 Point3D ptOrigin(0.0, 0.0, 0.0);
17 Vector3D vOriginToStart(ptSphereStart - ptOrigin);
18 Vector3D vOriginToEnd(ptSphereEnd - ptOrigin);
19 Vector3D vRotate = vOriginToEnd.CrossProduct(vOriginToStart);
20 vRotate.Normalize();
21
22 // ----------------求旋转角度----------------
23 double dTheta = std::acos(std::max(std::min(vOriginToStart.DotProduct(vOriginToEnd), 1.0), -1.0));
24
25 // ----------------生成旋转矩阵----------------
26 return Matrix4X4::CreateRotateMatrix(dTheta, vRotate);
27 }
28
29 Point3D ArcBall::ProjectPointToSemiSphere_i(const Point3D& ptPlane)
30 {
31 double dSquare = ptPlane.x * ptPlane.x + ptPlane.y * ptPlane.y;
32 double dSphereZ = dSquare >= 1.0 ? 0.0 : std::sqrt(1 - dSquare);
33
34 return Point3D(ptPlane.x, ptPlane.y, dSphereZ);
35 }
代码实现不是很难,在此不作过多解释。有一点说明一下,输入的顶点是变换后世界坐标系下的坐标点。关于怎么把屏幕像素坐标点转换为OpenGL坐标系下的点我们已经在3.1中作了说明。
3.4 客户端代码和实现效果
在客户端的main函数中需要向OpenGL注册鼠标状态改变时的回调函数和鼠标按住时移动的回调函数,如下:
glutMouseFunc(display_mouse_state_changed);
glutMotionFunc(display_mouse_move);
在鼠标状态改变回调函数中,鼠标左键按住表示旋转,鼠标中键按住表示平移,鼠标右键按住表示缩放。两个回调函数的具体代码如下(红色是左键旋转的逻辑、绿色是中键平移的逻辑、蓝色为右键缩放的代码逻辑):
1 void display_mouse_state_changed(int button, int state, int x, int y)
2 {
3 ptStart = Point2DHelper::ConvertPointFromScreenToWorld(Point2D(x, y));
4 if (GLUT_LEFT_BUTTON == button)
5 {
6 if (state == GLUT_DOWN)
7 {
8 bIsRotate = true;
9 }
10 else if (state == GLUT_UP)
11 {
12 bIsRotate = false;
13 }
14 }
15 else if (GLUT_MIDDLE_BUTTON == button)
16 {
17 if (state == GLUT_DOWN)
18 {
19 bIsMove = true;
20 }
21 else if (state == GLUT_UP)
22 {
23 bIsMove = false;
24 }
25 }
26 else if (GLUT_RIGHT_BUTTON == button)
27 {
28 if (state == GLUT_DOWN)
29 {
30 bIsScale = true;
31 }
32 else if (state == GLUT_UP)
33 {
34 bIsScale = false;
35 }
36 }
37 }
38
39 void display_mouse_move(int x, int y)
40 {
41 if (!bIsMove && !bIsRotate && !bIsScale)
42 {
43 return;
44 }
45
46 // ------像素坐标点的XY坐标转换为世界坐标点的XY坐标------
47 ptEnd = Point2DHelper::ConvertPointFromScreenToWorld(Point2D(x, y));
48
49 // --------------准备矩阵数据--------------
50 Matrix4X4 mat;
51 if (bIsRotate)
52 {
53 mat = ArcBall::CreateRotateMatrix(ptStart, ptEnd);
54 }
55 else if (bIsMove)
56 {
57 mat = Matrix4X4::CreateTranslateMatrix(ptEnd - ptStart);
58 }
59 else if (bIsScale)
60 {
61 double dLength = (ptStart.x - ptEnd.x) + (ptEnd.y - ptStart.y);
62 double dScale = std::exp(dLength);
63 mat = Matrix4X4::CreateScaleMatrix(dScale);
64 }
65 mat_transform = mat * mat_transform;
66
67 // --------------上传矩阵数据--------------
68 glUniformMatrix4fv(mat_location, 1, GL_TRUE, mat_transform._m);
69 glutPostRedisplay();
70
71 ptStart = ptEnd;
72 }
本想在贴上交互效果动画的,无奈图片较大,上传屡次不成功,且考虑到一些园友流量有限,所以就把这些实验结果放到百度网盘上,以供园友们学习借鉴~地址为:http://pan.baidu.com/s/1hsbcGK0,这是读书笔记(一)~读书笔记(四)的所有代码。如果想直接运行的话,需要解压至 F:\VC++游戏编程 路径下,否则会导致着色器程序找不到。下次博主将会更新,解压后可以存放在任意位置。
解压后有两个文件夹:
OpenGLGuide:用于存放源代码;
Package:用于存放编译出来的lib、dll以及头文件。
4 总结
至此,我们陆陆续续学习了三维变换的一些知识,包括平移、旋转和缩放矩阵的理论推导与应用。在下一次读书笔记中,我们将继续三维变换的内容——投影变换矩阵的推导与应用,这应该是三维变换的最后一块内容了。五一三天假期马上结束了,又要开始上班了。
OpenGL学习之路(四)的更多相关文章
- OpenGL学习之路(五)
1 引子 不知不觉我们已经进入到读书笔记(五)了,我们先对前四次读书笔记做一个总结.前四次读书笔记主要是学习了如何使用OpenGL来绘制几何图形(包括二维几何体和三维几何体),并学习了平移.旋转.缩放 ...
- OpenGL学习之路(一)
1 引子 虽然是计算机科班出身,但从小对几何方面的东西就不太感冒,空间想象能力也较差,所以从本科到研究生,基本没接触过<计算机图形学>.为什么说基本没学过呢?因为好奇(尤其是惊叹于三维游戏 ...
- OpenGL学习之路(三)
1 引子 这些天公司一次次的软件发布节点忙的博主不可开交,另外还有其它的一些事也占用了很多时间.现在坐在电脑前,在很安静的环境下,与大家分享自己的OpenGL学习笔记和理解心得,感到格外舒服.这让我回 ...
- Redis——学习之路四(初识主从配置)
首先我们配置一台master服务器,两台slave服务器.master服务器配置就是默认配置 端口为6379,添加就一个密码CeshiPassword,然后启动master服务器. 两台slave服务 ...
- OpenGL学习之路(二)
1 引子 在上一篇读书笔记中,我们对书本中给出的例子进行详细的分析.首先是搭出一个框架:然后填充初始化函数,在初始化函数中向OpenGL提供顶点信息(缓冲区对象)和顶点属性信息(顶点数组对象),并启用 ...
- zigbee学习之路(四):按键控制(中断方式)
一.前言 通过上次的学习,我们学习了如何用按键控制led,但是在实际应用中,这种查询方式占用了cpu的时间,如果通过中断控制就可以解决这个问题,我们今天就来学习按键控制的中断方式. 二.原理分析 传统 ...
- OPENGL学习之路(0)--安装
此次实验目的: 安装并且配置环境. 1 下载 https://www.opengl.org/ https://www.opengl.org/wiki/Getting_Started#Downloadi ...
- OpenGL学习笔记(四)纹理
目录 要完成的纹理效果 纹理环绕方式 纹理过滤 多级渐远纹理 加载与创建纹理 stb_image库的使用方法 生成纹理对象 应用纹理 纹理单元 参考资料:OpenGL中文翻译 要完成的纹理效果 纹理是 ...
- [原创]java WEB学习笔记79:Hibernate学习之路--- 四种对象的状态,session核心方法:save()方法,persist()方法,get() 和 load() 方法,update()方法,saveOrUpdate() 方法,merge() 方法,delete() 方法,evict(),hibernate 调用存储过程,hibernate 与 触发器协同工作
本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...
随机推荐
- C Primer Plus之结构和其他数据形式
声明和初始化结构指针 声明结构化指针,例如: struct guy * him; 初始化结构指针(如果barney是一个guy类型的结构),例如: him = &barney; 注意:和数组不 ...
- lintcode:完美平方
题目 给一个正整数 n, 找到若干个完全平方数(比如1, 4, 9, ... )使得他们的和等于 n.你需要让平方数的个数最少. 样例 给出 n = 12, 返回 3 因为 12 = 4 + 4 + ...
- hdu 1848 Fibonacci again and again (初写SG函数,详解)
思路: SG函数的应用,可取的值为不连续的固定值,可用GetSG求出SG,然后三堆数异或. SG函数相关注释见代码: 相关详细说明请结合前一篇博客: #include<stdio.h> # ...
- PushBackInputStream与PushBackInputStreamReader的用法
举个例子:获取XX内容 PushBackInputStream pb=new PushBackInputStream(in,4);//4制定缓冲区大小 byte[] buf=new byte[4]; ...
- IOS中表视图(UITableView)使用详解
IOS中UITableView使用总结 一.初始化方法 - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)styl ...
- Python模块包中__init__.py文件的作用
转载自:http://hi.baidu.com/tjuer/item/ba37ac4ce7482a0f6dc2f08b 模块包: 包通常总是一个目录,目录下为首的一个文件便是 __init__.py. ...
- Mysql笔记——触发器简单实例
首先贴上触发器语法吧: CREATE TRIGGER <触发器名称> –触发器必须有名字,最多64个字符,可能后面会附有分隔符.它和MySQL中其他对象的命名方式基本相象. { BEFOR ...
- 让浏览器屏蔽js
有时候为了测试js是否做到了渐进增强,需要屏蔽下js 做法: Internet选项>>安全>>自定义级别 禁用java小程序脚本和活动脚本 有的浏览器调试器直接就有这个功能
- 使AJAX调用尽可能利用缓存特性
优化网站设计(十四):使AJAX调用尽可能利用缓存特性 前言 网站设计的优化是一个很大的话题,有一些通用的原则,也有针对不同开发平台的一些建议.这方面的研究一直没有停止过,我在不同的场合也分享过这样的 ...
- Model元数据解析
Model 元数据是针对数据类型的一种描述信息,主要用于控制数据类型本身及其成员属性在界面上的呈现方式,同时也为Model 绑定和验证提供必不可少的元数据信息.一个复杂数据类型通过属性的方式定义了一系 ...