OpenGL超级宝典(第7版)笔记20 统一变量 一致区块 uniform相关内容 清单5.9-5.28

文章目录

  • OpenGL超级宝典(第7版)笔记20 统一变量 一致区块 uniform相关内容 清单5.9-5.28
  • 前言
  • 1 统一变量 (uniform变量)
  • 2 在一致区块之前
  • 3 一致区块(uniform块)
    • 3.1uniform块基础介绍
    • 3.2标准布局std140,数据准备
    • 3.3默认的shared布局,数据准备
    • 3.4 准备好数据后进行传入
  • 6 总结

前言

上一篇我们用顶点数组和缓冲区向着色器传输顶点数据,避免了在着色器中内置大量的数据,方便了我们添加顶点。虽然现在的顶点还是通过数组的方式,我们还是要手动输入,但是相比内置在着色器中,我们在自己的内存中可以更灵活的对它进行修改。当然我们并不会一直手动输入顶点,等我们把第五章的内容全部介绍完之后,我们再说一说怎么通过读取obj文件来自动获取顶点数据,到时候直接读取建模软件生成的obj文件就行了(obj文件中含有成千上万的顶点,可以表示很复杂的几何图形)。

说回来,这一篇我们将介绍统一变量(或叫做uniform变量),uniform变量也可以向着色器传递数据,而且数据修改起来更方便,数据的形式也更多。通过uniform变量我们可以占用一个顶点着色器上的一个location来传入一个矩阵(现在我们不是占用了4个位置才传入变换矩阵吗)。而且uniform变量可以传递到不同的着色器中去(之前我们用缓冲的方法只能输入到VS中,再通过着色器之间传递才能让其他着色器获取数据,而uniform可以直接传到对应的着色器中去)。

1 统一变量 (uniform变量)

还记的我们之前在传矩阵的时候调用了4个location来传入,十分的麻烦:

//顶点着色器中的4个向量组成变换矩阵
layout (location=0) in vec4 cam0;
layout (location=1) in vec4 cam1;
layout (location=2) in vec4 cam2;
layout (location=3) in vec4 cam3;//每次渲染render()的时候都要传入四个位置的向量,才能在着色器中合成矩阵
glVertexAttrib4fv(0, &matrix_ok.m_mat4f[0]);
glVertexAttrib4fv(1, &matrix_ok.m_mat4f[4]);
glVertexAttrib4fv(2, &matrix_ok.m_mat4f[8]);
glVertexAttrib4fv(3, &matrix_ok.m_mat4f[12]);

如果有一种方法能够一次就把这些数据传递给着色器就好了。我们的方法就是uniform变量。

首先说说uniform变量的特点:

1,uniform变量之所以叫统一(即uniform),是因为其在基元处理的过程中其值并不会发生改变,比如说我们传入变换矩阵,在顶点绘制的过程中,物体上的每个顶点进行变换的时候都是用同一个矩阵进行变换,矩阵中的数据是统一的,这样避免了渲染的时候不同顶点使用不同的变换矩阵进行变换(那样会导致画面扭曲)

2,任何的着色器变量都可以被指定为uniform变量,uniform变量可以存着于任何着色器阶段。

3,uniform变量可以直接传入到任意的着色器中去,不像顶点属性:

#version 450 core //清单3.1
layout (location = 0) in vec4 offset;
void main(void){const vec4 vertices[3] = vec4[3](vec4(0.25,-0.25,0.5,1.0),vec4(-0.25,-0.25,0.5,1.0),vec4(0.25,0.25,0.5,1.0));gl_Position = vertices[gl_VertexID]+offset;
}

只能传到顶点着色器中去,想要给片段着色器传数据还要用in out来实现。uniform可以直接把数据传到片段着色器中,不经过顶点着色器。例如:

#version 330 core
in vec3 ourColor;
in vec2 TexCoord;//平面贴图纹理坐标
in vec3 Normal0;//片元在世界中的法线方向,用于光照计算
in vec3 apos;//片元在世界中的位置,用于光照计算
in vec4 shadowpos;//点在阴影变换后的坐标 out vec4 color;
out vec4 FragColor;//最终的片元颜色 uniform sampler2D ourTexture;//物体平面贴图
uniform sampler2D shadowTexture;//二次渲染阴影贴图
struct DirectionalLight{vec3 Color;//光线颜色float AmbientIntensity;//全局光照线性强度vec3 Lightpos;//点光源位置...
};
uniform DirectionalLight dlight;uniform float constant;//光源衰减系数 常量
uniform float linear;//光源衰减系数 一次系数
uniform float exp;//光源衰减系数 二次系数 void main(){...略
}

这里的dlight、constant、linear等就是直接传到FS中去的,没有经过VS。

我们现在来通过uniform变量把摄像机的变化矩阵传递到着色器中吧!
先是修改一下顶点着色器,为uniform变量腾出一个位置,并删除之前的那几个:

#version 450 core
layout (location=0) uniform mat4 cam; //分配0号位置给它
layout (location=4) in vec3 vsposition;
layout (location=5) in vec3 vscolor;
out vec4 fscolor;
void main(void)
{                                        fscolor = vec4(vscolor,1.0);          gl_Position = cam * vec4(vsposition,1.0);
}

是不是比之前好看多了~,之后我们要在每次绘制图像之前(render()中)传入矩阵数据(通过uniform变量)我们将用到glUniformMatrix4fv()这个函数,它能直接向对应的location位置上的uniform变量传递数据。其原型如下:

void glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat * value);

第一个参数表示传到那个location上去,第二个参数表示传入几个矩阵(有的是uniform变量数组,比如layout (location=0) uniform mat4 cam[3];,那时候就要传入3个矩阵了),第三个参数表示传入的数据是行优先(GL_TRUE)还是列优先(GL_FALSE)的,第四个参数指向待传入数据的指针。
我们这里应该为:

    glUniformMatrix4fv(0,1, GL_TRUE,camera01.m_Transformation.m_mat4f);//location=0 传入1个矩阵 行优先 数据的指针为camera01.m_Transformation.m_mat4f

如果你做的没错,你运行之后应该和之前的结果是一样的,但是我们把之前很麻烦的传入方法改成了现在很简单的一两行代码,以后我们会经常使用uniform变量来传递数据。

2 在一致区块之前

当然我们并不能满足于仅仅是使用uniform变量来传递些数据,我们还希望传递一些整块的数据,或是向结构成员中传入数据,比如之前例子中的:

struct DirectionalLight{vec3 Color;//光线颜色float AmbientIntensity;//全局光照线性强度vec3 Lightpos;//点光源位置...
};
uniform DirectionalLight dlight;

这样我们在着色器中使用的时候之间用dlight.Color就可以调用数据了,比较易懂。那么怎么传入数据到dlight中呢?其实很简单,虽然dlight中的Color没有写location=…但是其实OpenGL还是自动为它分配了位置的(只不过不是我们手动设置的),我们可以通过glGetUniformLocation();函数查询它的location是多少:

 GLint amblight_loc01 = glGetUniformLocation(pro.m_program,"dlight.Color");

之后我们在绘制前传入数据就行了:

glUniform3f(amblight_loc01,amblight.x,amblight.y,amblight.z);

如果dlight中的每个成员都这样传递完成,就在着色器中可以用dlight.Color来使用它们了。

虽然我们实现了所谓的“一整块”数据的传输,但是其实在渲染前还是要一个一个的调用glUniform*();函数,还是有很大的开销,下面就要介绍uniform块,uniform块能够避免这种情况,一次性将数据传递进去。

3 一致区块(uniform块)

3.1uniform块基础介绍

上面提到虽然在着色器中是以结构的方式调用的,但是我们还是把数据一点一点的传入的,仍不能实现真正的一次性传输,于是uniform块就出现了!
首先是uniform块的声明:

uniform TransformBlock{bool open;mat4 camera;float iso;          vec4 fscolor1;
} transform;

注意的是TransformBlock是这类uniform块的名称,transform是该uniform块的名称。(就有点类似于class student{…} zhangsan,lisi;其中类的名字是student,而具体学生的名字是zhangsan和lisi)
命名好uniform块之后需要向缓冲中传入数据(就是传到uniform块中),但是我们在传入前要怎样准备数据块呢,数据块中的数据布局是什么样的呢,这就牵扯出uniform块的两种数据布局情况。

一是使用标准的、共识的数据布局(标准布局)。二是由OpenGL自己决定数据的布局方式(shared布局)
标准布局:由于uniform块中的数据所占空间大小不一(如上面的open占1字节,而fscolor1占16字节,而iso只占4字节)顺序也是非常的乱,在写入数据和读取数据的时候很麻烦(有时候指针要移动1字节,有时候是4字节,有时候是16字节),但是在标准布局下所有的数据都按照一个规定的规则进行存放,某些数据之前会预留一些空间,这导致缓存会比所需空间更大,但是由于是标准的,每个数据有固定的位置,所以在传入数据的时候可以直接把在硬盘中准备好的数据直接读进去,十分方便。
shared布局:由于是OpenGL自己决定数据的布局,所以某些情况下可以生成更有效的着色器。但是由于是OpenGL自己决定的布局,所以我们在程序运行前是不知道数据怎样布局的,这需要我们在运行的时候用函数进行查询,查出数据的布局形式后,再准备数据来传递,所以在应用程序层面需要更多的工作量。

下面的布局是默认的shared布局:

//默认的shared 布局
layout uniform TransformBlock{  bool open;                  mat4 camera;                float iso;                  vec4 fscolor1;
} transform;

下面是标准布局std140:

//标准布局 std140
layout uniform (std140) TransformBlock{ bool open;                          mat4 camera;                        float iso;                          vec4 fscolor1;
} transform;

3.2标准布局std140,数据准备

由于标准布局是有统一的布局规则的,所以你可以在程序运行之前就确定它的布局情况,可以提前根据布局情况,准备数据。

具体的布局规则如下:
1,对于int float bool等类型,其起始位置都是4字节的整数倍处
2,对于vec2 来说,其起始位置都是8字节的整数倍处
3,对于vec3 vec4来说,其起始位置都是16字节的整数倍处
4,对于数组而言,每个元素的起始位置都是16字节的整数倍处
5,对于矩阵来说,不管是mat3 mat4,其都看成是vec3 vec4的数组,每个元素的起始位置都是16字节的整数倍处。
6,对于结构数组来说,每个元素的起始位置都是其最长成员的边界处,也就是16字节的整数倍处。

比如说下面的uniform块:

layout uniform (std140) TransformBlock{  bool open;          //起始:0   对齐基数:4   占用空间:4*1 结束:4mat4 camera;     //起始:16  对齐基数:16  占用空间:16*4    结束:80float iso;          //起始:80  对齐基数:4   占用空间:4*1 结束:84vec4 fscolor1;      //起始:96  对齐基数:16  占用空间:16*1    结束:112
} transform;

再比如说书上的那个:

layout uniform (std140) TransformBlock{  float scale;                //起始:0   对齐基数:4   占用空间:4*1 结束:4vec3 translation;            //起始:16  对齐基数:16  占用空间:16*1    结束:32float rotation[3];          //起始:32  对齐基数:16  占用空间:16*3    结束:80mat4 projection_matrix;     //起始:80  对齐基数:16  占用空间:16*4    结束:144
} transform;

我们在判断起始位置的时候要先给出对齐基数,然后将上一个成员的结束位置和对齐基数进行比较,得到新成员的起始位置。比如说translation的对齐基数是16,而上一个成员scale的结束位置是4,4并不是16的整数倍,所以translation的起始位置后移成为16。

当然你也可以手动设置起始位置,但是仍要遵循std140的布局标准(即对齐基数的整数倍),比如:

layout uniform (std140) TransformBlock{  layout (offset=80) bool open;  //起始:80  对齐基数:4   占用空间:4*1 结束:84layout (offset=16) mat4 camera;    //起始:16  对齐基数:16  占用空间:16*4    结束:80layout (offset=0) float iso;   //起始:0   对齐基数:4   占用空间:4*1 结束:4vec4 fscolor1;                   //起始:96  对齐基数:16  占用空间:16*1    结束:112
} transform;

这里就把open和iso的位置调换了一下,当然你也可以通过手动设置在成员之间预留出更多的空间,以备以后扩展。

当然如果你觉得对齐的基数有的是4有的是8有的是16 太复杂了,你可以用align来统一对齐的基数,例如:

layout uniform (std140,align=16) TransformBlock{    //这样对齐的基数都是16了...
} transform;

这样根据我们计算的偏移直接准备数据就行了(具体就不操作了,用malloc 和指针赋值就行了)

3.3默认的shared布局,数据准备

对于shared布局而言,由于是OpenGL自己决定每个变量的起始位置,所以我们在程序运行之前是无法知道OpenGL是怎样安排的,所以我们只能在OpenGL完成布局后,查出布局的情况,通过OpenGL自己提供的函数来进行查找。这里我们用到的函数是glGetUniformIndices和glGetActiveUniformsiv两个函数(分别查询成员的索引,和根据索引获取成员的信息)

首先我们要获取成员的索引(或者说是编号),只有通过索引我们才能查找有关成员的起始位置等信息。

static const GLchar* uniformnames[4] = {"TransformBlock.open","TransformBlock.camera","TransformBlock.iso","TransformBlock.fscolor1" };GLuint uniformindices[4];glGetUniformIndices(rendering_program, 4, uniformnames, uniformindices);//获取到4个索引,并放到数组中

这里要注意的是uniform的名字是TransformBlock而不是我们的transform,因为是shared布局,所以说所有的TransformBlock类型下的uniform块都会是统一的布局,所以我们查找的时候直接查找TransformBlock…就行了。

接下来就是要根据索引调用glGetActiveUniformsiv,获取每个成员的起始位置、数组元素的偏移量(步长),矩阵每行/列之间的偏移量(步长)。

GLint uniformoffsets[4];GLint arraystrides[4];GLint matrixstrides[4];glGetActiveUniformsiv(rendering_program, 4, uniformindices, GL_UNIFORM_OFFSET, uniformoffsets);//open、camera、iso、fscolor1 这些成员的偏移量glGetActiveUniformsiv(rendering_program, 4, uniformindices, GL_UNIFORM_ARRAY_STRIDE, arraystrides);//如果有数组成员,那么arraystrides就是数组的每个元素之间的偏移量,由于我们这里没有数组,所以用不到(跟矩阵的差不多)glGetActiveUniformsiv(rendering_program, 4, uniformindices, GL_UNIFORM_MATRIX_STRIDE, matrixstrides);//如果有矩阵成员,那么matrixstrides就是矩阵的每行/列之间的偏移量,由于我们这里只有camera是矩阵,所以只有matrixstrides[1]能够用得上,其他的都没用

好的我们现在查得了所有的偏移量信息,现在要准备数据,按照这些偏移量信息“装车”了。

 unsigned char* uniform_buffer = NULL;//这个放在全局变量中uniform_buffer = (unsigned char*)malloc(1024);//扩展一片区域用于放数据*((bool*)(uniform_buffer+uniformoffsets[0])) = 1;//设置open的数值为1*((float*)(uniform_buffer+uniformoffsets[2])) = 1.0f;//设置iso的数值为1.0f ((float*)(uniform_buffer+uniformoffsets[3]))[0] = 0.5f;((float*)(uniform_buffer+uniformoffsets[3]))[1] = 0.5f;((float*)(uniform_buffer+uniformoffsets[3]))[2] = 0.5f;((float*)(uniform_buffer+uniformoffsets[3]))[3] = 1.0f;//设置 vec4 fscolor1 的数值为(0.5f,0.5f,0.5f,1.0f)//这里是先用uniform_buffer指针加上偏移量uniformoffsets,之后做强制类型转化,转化为(float*),在对其进行赋值,这样数据就放到对应的位置上去了unsigned int offset = uniformoffsets[1];for (int i = 0;i < 4;i++) {offset = uniformoffsets[1] + matrixstrides[1] * i;for (int j=0;j<4;j++){*((float*)(uniform_buffer + offset)) = matrix_ok.m_mat4f[j*4+i];//这里注意一下是j*4+i不是i*4+j,因为我们的数组是行优先,而OpenGL中是列优先offset += sizeof(GLfloat);}}//准备矩阵数据的时候有点麻烦,对于第i行j列的数据,指针要先加上整个矩阵的偏移(uniformoffsets[1],即整个矩阵是从这个位置开始的),//再加上该行的偏移( matrixstrides[1] * i,即第i行是从这个位置开始的),//再加上该列的偏移(即在最内侧的里加上offset += sizeof(GLfloat);),三个偏移加上后才能进行赋值

这里比较难理解的是矩阵或是数组的偏移问题,因为我们要先找到该矩阵的起始点(即uniformoffsets[1]),后要找到该行的起始点(uniformoffsets[1]+matrixstrides[1] * i),最后找到该行中该元素的位置(uniformoffsets[1]+matrixstrides[1] * i+sizeof(GLfloat) * j),最后进行赋值。
(注:matrix_ok.m_mat4f为我们之前计算好的、需要传入着色器的变换矩阵,具体可见笔记17、18)

3.4 准备好数据后进行传入

准备好数据之后需要建立缓冲对象UBO(有点类似于VBO),跟VBO很像,只不过储存的信息是不同的:

 GLuint Uniform_buffer;glCreateBuffers(1, &Uniform_buffer);glNamedBufferStorage(Uniform_buffer, 64*64, uniform_buffer, GL_MAP_WRITE_BIT | GL_DYNAMIC_STORAGE_BIT);

接下来是一些绑定操作:

 GLuint Transformblock_index = glGetUniformBlockIndex(rendering_program, "TransformBlock");glUniformBlockBinding(rendering_program, Transformblock_index, 3);//由于我们没在着色器中的uniform块上写binding=3,所以要调用glUniformBlockBinding让TransformBlock绑定在3上glBindBufferBase(GL_UNIFORM_BUFFER, 3, Uniform_buffer);

这里在做的事情是先把获取了"TransformBlock"的索引(其实用location=…也可以),然后把对应的索引和GL_UNIFORM_BUFFER的3号位置绑定起来,最后把GL_UNIFORM_BUFFER的3号位置和Uniform_buffer的缓冲区UBO绑定起来,最终实现uniform块的数据传输。
具体示意图如下(分为两种方法,两个方法都行):

当然别忘记在render中也要传入矩阵数据(要不然矩阵数据就得不到更新)

unsigned int offset = uniformoffsets[1];for (int i = 0;i < 4;i++) {offset = uniformoffsets[1] + matrixstrides[1] * i;for (int j = 0;j < 4;j++){*((float*)(uniform_buffer + offset)) = matrix_ok.m_mat4f[j * 4 + i];offset += sizeof(GLfloat);}}glNamedBufferSubData(Uniform_buffer , 0 , 64 * 64 , uniform_buffer);

最后我们能看到我们的结果和直接用uniform变量是一样的。

6 总结

我们这篇为大家介绍了uniform变量和uniform块的实用,其中有许许多多要注意的点,uniform变量和块使得我们可以更方便的传入数据到着色器中去,但是我们的着色器并不能对其数据进行修改,这非常难受,我们在下一篇中会为大家介绍着色器储存区块,到时候看我们是怎么使用并解决这样的问题的。
那我们下一篇见~~
注:你可以在我的资源中找到代码文件

参考文章:
1,OpenGL学习脚印: uniform blocks在着色器中的使用

关于书籍的问题
如果你手中没有该书,我还是建议你购买一本,毕竟书本毕竟更加严谨专业,我这里难免遗漏一些细节,主要是提供实例,并做一个消化,将很混乱的流程为大家理清,但这笔记一定是通俗的,是对新手友好的(当然有时候你需要在某些方面自己努努力,比如后面出现的基本线性代数的内容,还有C语言或是c++的基础知识,虽然我可能也不太懂O(∩_∩)O,慢慢来吧)。

别被吓住
刚开始的时候很容易被OpenGL的巨长的函数和超级复杂的流程吓到,其实并没有那么可怕,只要对这样或那样的流程熟悉之后,一切都变得相当简单(当然如果你能提出一个更好的流程那就更好了,当我们把很多基础的工作做完,我们会不断的提出新问题新点子,用新的技术来实现它,最终完成OpenGL的学习)

虽然我也不知道后面将是怎样的道路,但至少努力学习是没错的。

我看过的相关内容
以下并不是全看完了,大部分看了15%就看不下去了,实在是没看懂。(本人没什么计算机编程基础,算是野生程序员吧,很多内容都不能标准表述,望见谅)
如果你对opengl的工作有了一定的了解,我一开始也是从这里开始的,但是仍然有很多的不懂的,最后至今为止,我杂糅了很多的网站内容包括LearnOpenGL、极客学院、哔哩哔哩的闫令琪计算机图形学(闫令琪又开202课程了,可以围观一下)、哔哩哔哩的傅老师的OpenGL课程、OpenGL编程指南"也称为红宝书"、OpenGL超级宝典"也称为蓝宝书"、当然还有很多的csdn文章O(∩_∩)O这就不介绍了,等用到是时候我在放链接吧O(∩_∩)O

这里面闫令琪的图形学比较易懂也很基础推荐可以作为开始(如果你是学OpenGL需要马上用,应该可以跳过,但是其中的内容很是很重要,这会让马上要涉及变换透视的章节更加易懂,推荐大家看看),之后是蓝宝书或是极客学院翻译的教程比较推荐,这两个还是比较适合你我这样的新手的。
这里不推荐看的是红宝书,这本书我看了有点类似于字典那样的工具书,不太适合新手上手学,而且讲的也并不是很通俗易懂(可能是我的书版本比较老吧…)

加油
当然如果你对我有信心,我也会持续更新(虽然前路漫漫),跟大家一同进步(虽然很可能没人看(╥╯^╰╥),无所谓了,当然如有错误还请大家指正∠(°ゝ°),哪里不懂我会尽力解决,哪里说的不好也可以指出我会及时修改~)

我们下篇见~~

OpenGL超级宝典(第7版)笔记20 统一变量 一致区块 uniform相关内容 清单5.9-5.28相关推荐

  1. 《OpenGL超级宝典第5版》学习笔记(一)—— 第一个OpenGL程序

    // GLTools库包含了一个用于操作矩阵和向量的3D数学库,并依靠GLEW获得OpenGL3.3中用来产生和渲染一些简单3D对象的函数, // 以及对视觉平截头体.相机类和变换矩阵进行管理的函数的 ...

  2. OpenGL超级宝典第7版环境配置

    1.下载源码 地址:http://www.openglsuperbible.com/ 2.运行Cmake,编译glfw库 打开项目后,编译工程(Debug和Release下都编译). 把E:\open ...

  3. OpenGL超级宝典(第7版)笔记4 渲染管线介绍 清单2.3-2.7

    OpenGL超级宝典(第7版)笔记4 渲染管线介绍 清单2.3-2.7 文章目录 OpenGL超级宝典(第7版)笔记4 渲染管线介绍 清单2.3-2.7 1 OpenGL简介 2 OpenGL渲染管线 ...

  4. OpenGL超级宝典学习笔记——操作矩阵

    为了更强大的功能和灵活性,我们有时需要直接操作矩阵.在OpenGL中4x4的矩阵用包含16个浮点数值的一维数组来表示,而不是用二维的4x4的数组来表示.OpenGL之所以这么做,因为使用一维数组更高效 ...

  5. OpenGL超级宝典(第五版)环境配置

    本文转自:http://blog.csdn.net/sunny_unix/article/details/8056807,感谢作者分享. OpenGL超级宝典(第五版)环境配置 Vs2008+winX ...

  6. OpenGL超级宝典(第五版)环境配置【转】

    OpenGL超级宝典(第五版)环境配置 Vs2008+winXP  后续会整理Ubuntu 12.04LTS下的配置作者:sunny_unix 1.各种库的配置 (1)glew 下载:https:// ...

  7. OpenGL超级宝典(第五版) 环境配置

    特别提醒:有些在word中或者其他中的代码复制到vs中会报错,原因是word中有些隐含的字符,复制到vs中就会报错:重新输一遍就可以解决问题,这里只是提醒下! 可以参阅我前面转载的一篇文章,进行比较然 ...

  8. OpenGL超级宝典(第五版) 环境配置(WinXp+VS2008)

    转自:http://blog.csdn.net/sunny_unix/article/details/8056807 OpenGL超级宝典(第五版)环境配置 1.各种库的配置 (1)glew 下载:h ...

  9. [转]OpenGL超级宝典 5e 环境配置

    OpenGL超级宝典(第五版)环境配置 1.各种库的配置 (1)glew 下载:https://sourceforge.net/projects/glew/files/glew/1.7.0/glew- ...

最新文章

  1. 邮件回复功能失效 谁遇到过?
  2. xgboost、随机森林和逻辑回归的优缺点
  3. java八种排序算法---直接插入排序
  4. 世界最早投入运行的计算机网络是,世界最早投入运行的计算机网络是
  5. 深入理解JavaScript中的this关键字
  6. laravel5.5 php7,ubuntu 16.04+nginx+mysql+php7.1+laravel5.5环境
  7. 悲哀!面试现场,简单几道java算法题,90%程序员没写出来
  8. Linux学习笔记:GDB常用命令
  9. docker 启动tomcat_docker安装tomcat
  10. Oracle 隔离级别
  11. windows系统vbs脚本 恶搞关不掉的窗口 以及解决办法
  12. Nacos教程_2 讲解
  13. 从数据黑盒到数据白盒,阿里云基础产品首席架构师黄瑞瑞分享背后的故事
  14. 源码安装php5.5
  15. python对seo有什么用_python对seo的帮助 – python对seo的帮助是什么?- 企业服务
  16. 毕业生的找工作的时候住宿问题是如何解决的
  17. kinect沙池游戏
  18. Unresolveable build extension: Plugin...or one of its dependencies could not be resolved: Failed to
  19. 2015合肥市第 32 届青少年信息学(计算机)奥林匹克竞赛小学组试题
  20. 海上平台作业三维虚拟仿真

热门文章

  1. 【转】期刊投稿信和催稿信
  2. 一个简单评价机器学习预测效果的办法
  3. zigzag扫描matlab,Zigzag逆扫描
  4. 推荐引擎 - Summary Analysis
  5. 联想微型计算机开机密码忘记了,联想笔记本忘记开机密码怎么办
  6. python计算乘积_python中矩阵运算(乘法和数量积)
  7. 链栈的定义、构建、入栈、出栈和取栈顶元素
  8. jquery版选项卡
  9. javascript中加减时间
  10. ios::exceptions()函数