前言:这一章我们将对前面的所有知识进行实践,通过OpenGL编程,完成一个简单的OpenGL程序。你可以通过访问我的GitHub获取我学习的HelloOpenGL项目。这个项目需要的所有头文件及库均配置在项目中,下载后无需再额外配置环境,但由于作者并不想重复配置多个平台,请在Debug - Win32(x86)环境下编译。另外,作者把很多理解都写在了注释中!!!

项目地址:https://github.com/Jnnes/HelloOpengl

引用网址:https://learnopengl-cn.readthedocs.io/zh/latest/ 如遇到问题,请上该网址查找。

OpenGL编程前须知:在开始OpenGL编程之前,我们需要提前了解一些东西,包括以下几方面

  1. OpenGL是一个大的状态机,我们可以把渲染的每个阶段都看成一个状态,而在每个状态的动作由各种属性控制,当前状态的动作执行完后会自动进入下一个状态,直到完成渲染,我们需要使用属性设置函数来设置属性,通过属性应用函数来应用属性。
  2. 在状态机中,我们把很多东西都当做对象来管理,这样当状态机中有多个相同类型的对象时,我们可以通过绑定某种类型对象和他实际存储数据发挥作用的对象,那么当状态机需要使用该类型时,则会自动去使用绑定该类型的对象。例如:纹理对象,缓冲对象等。所有的对象都继承自Object,对于这种使用对象来管理的数据,我们在使用之前都需要创建一个对象,然后将该类型对象绑定到新创建的这个对象实例上。
// OpenGL的状态
struct OpenGL_Context
{...object* object_Window_Target;...
};// 创建对象,后面都通过该对象id找到OpenGL中的这个对象
GLuint objectId = 0;
glGenObject(1, &objectId);// 绑定对象至上下文,指定GL_WINDOW_TARGET 类型的对象 存储在 objectId
glBindObject(GL_WINDOW_TARGET, objectId);// 设置GL_WINDOW_TARGET对象的一些选项,也即对 objectId的对象进行操作
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);// 将上下文的GL_WINDOW_TARGET对象设回默认,解除GL_WINDOW_TARGET 与objectId的绑定
// 后面对GL_WINDOW_TARGET的操作将不会操作objectId
glBindObject(GL_WINDOW_TARGET, 0);

但是,有些特殊对象并不是使用上面的函数,例如纹理:

// 创建一个纹理对象,并保存其唯一对象id,OpenGL内所有的对象Id均不相同,不论什么类型。
GLuint texture;
glGenTextures(1, &texture);// 绑定GL_TEXTURE_2D 的 对象ID为 texture
glBindTexture(GL_TEXTURE_2D, texture);// 往GL_TEXTURE_2D 中写数据,因为绑定的GL_TEXTURE_2D类型的对象的ID是texture,
// 那么下面的数据会写入到ID为texture的对象上
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);// 绑定GL_TEXTURE_2D 的 对象ID为 0,这段后面的代码如果操作了GL_TEXTURE_2D,将是在id为0
// 的纹理对象上,而id为零的纹理对象是不存在的,所以这句代码实际意思是解绑定GL_TEXTURE_2D 对象ID
glBindTexture(GL_TEXTURE_2D, 0);

其他还有VAO、VBO、VFO等等,虽然创建、绑定、修改这种对象时使用的函数名各不相同,但是他们都有一个统一的流程,这个是OpenGL设计理念决定的。他们都遵循创建,绑定,修改三步走,除非主动修改绑定对象,否则OpenGL上下文中的该类型对象绑定的ID始终不变。


正式编程:

下面代码省略include头文件,详细请看项目源代码。

1. 创建窗口

创建窗口的详细过程可以参照LearnOpengl入门的创建窗口(点击进入),或者使用文章开头提供的项目。下面只对代码进行解释。

因为OpenGL只是一个标准,只提供接口定义,内部实现由各大硬件厂商完成,并且还可以跨平台,所以我们需要一个库,它能够帮我们找到不同平台上的OpenGL函数到底在硬盘的哪里,glew库实现了这个功能。

另外我们可以使用glfw库方便我们创建窗口。

void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);int main()
{// 初始化glfwglfwInit(); // 设置OpenGL大版本号为3,小版本号为3,也即3.3glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);// 设置OpenGL使用Core模式,可使用可编程管线glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);// 禁用可变窗口大小glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);// 创建一个窗口对象,宽度800像素,高度600像素,标题为"LearnOpenGLGLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", nullptr, nullptr);if (window == nullptr){    // 退出程序前,一定要关闭glfwstd::cout << "Failed to create GLFW window" << std::endl;glfwTerminate();return -1;}glfwSetKeyCallback(window, key_callback); // 设置按键回调glfwSetScrollCallback(window, scroll_callback); // 设置鼠标移动回调glfwSetCursorPosCallback(window, mouse_callback); // 设置鼠标滚轮回调// 设置窗口的显示上下文,也即下面OpenGL绘制的内容将存储在window这个窗口对象上glfwMakeContextCurrent(window);// 从GLFW中获取视口的维度而不设置为800*600// 是为了让它在高DPI的屏幕上(比如说Apple的视网膜显示屏)也能正常工作。// 因为高DPI的屏幕上显示图片上的一个像素点可能需要几个屏幕发光单元int width, height;glfwGetFramebufferSize(window, &width, &height);glViewport(0, 0, width, height);    // 初始化glewif (glewInit()) {Log::e(TAG, "Failed init glew");glfwTerminate();return -1;}else {Log::i(TAG, "Init glew success");// 获取当前OpenGL可支持最大纹理数目GLint nrAttributes;// 我们可以通过glGetxxx来获取OpenGL上下文中的属性,glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);Log::i<std::string>(std::string(TAG), "max vertex attributes supportes:", std::to_string(nrAttributes));}// 我们需要一个主循环来连续绘制并刷新屏幕while(!glfwWindowShouldClose(window)){// 检查事件,包括按键、鼠标移动等等glfwPollEvents();// 因为数据是一个个像素绘制的,为了使一个屏幕的像素一起显示出来,OpenGL拥有双缓冲机制// 当某个缓冲绘制换成后,将其换到前台进行显示,另一个缓冲继续在后台绘制glfwSwapBuffers(window);}// 退出程序前,一定要关闭glfwglfwTerminate();return 0;
}void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode) {    // 如果按下返回键,则将应该关闭窗口标志位置位,退出main中的主循环if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {glfwSetWindowShouldClose(window, GL_TRUE);}
}void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{;
}void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{;
}

2. 缓冲对象

在创建着色器之前,我们需要定义顶点数据。

GLfloat vertices[] = { // 深度都为0,表现为在XY坐标系下的三个点-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f
};

我们需要使用VBO(顶点缓冲对象)来存储顶点数据,我们还可以使用VAO(顶点数组对象)来绑定,这样当我们切换不同的VBO时只需要切换绑定不同的VAO就可以了,更好的优点是,我们不仅可以吧VBO绑定在VAO上,我们还可以把VEO(顶点索引数组)也绑定在VAO上,这样我们只要切换了VAO就同时切换了VBO和VEO。

VAO、VBO、EBO之间的关系

我们可以像创建其他对象一样创建VAO,VBO,EBO

GLfloat vertices1[] = {//     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   2.0f, 0.0f,   // 右上 // 手动在这里翻转Y轴纹理坐标0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   2.0f, 2.0f,   // 右下-0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 2.0f,   // 左下-0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 0.0f    // 左上
};
GLuint indices1[] = {1,2,3,0,1,3,
};// 创建并绑定顶点缓冲数组
GLuint VAO1;
glGenVertexArrays(1, &VAO1);
glBindVertexArray(VAO1);// 创建、绑定 顶点索引缓冲对象,并设置数据
GLuint EBO1;
glGenBuffers(1, &EBO1);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO1);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices1), indices1, GL_STATIC_DRAW);// 创建、绑定 顶点缓冲对象,并设置数据
GLuint VBO1;
glGenBuffers(1, &VBO1);
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
// 从此刻起,我们使用的任何在GL_ARRAY_BUFFER目标上的缓冲调用都会用来配置当前VBO
// 然后我们可以调用GLBufferData函数,他会把之前定义的顶点数据复制到缓冲的内存中
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices1), vertices1, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void *)0);
glEnableVertexAttribArray(0);// 继续设置第2个顶点属性,对应的是着色器代码里的 layout(location = 1),和上图中VAO2的第二条黑线
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);// 继续设置第2个顶点属性,对应的是着色器代码里的 layout(location = 2)
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (void*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);// 取消之前顶点缓冲数组的绑定
glBindVertexArray(0);// 下面绘制的过程最好放在主循环中循环执行,除非你只是想绘制一次就再不刷新
// 我们可以在需要使用VBO 和 EBO时直接使用之前已经绑定好的VAO
glBindVertexArray(VAO1);
// 然后调用glDrawElement,OpenGL就会自动从已绑定的EBO中获取顶点索引,再根据索引在绑定的VBO中获取顶点坐标进行绘制
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, (void*)0);// 或者我们并不需要顶点索引,我们也可以通过其他方式绘制
// 直接绑定VBO,不使用EBO,然后调用glDrawArrays(), 就会从已绑定的VBO中按顺序获取坐标进行绘制
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
glDrawArrays(GL_TRIANGLES, 0, 3);     

在glBindVertexArray(VAO1)和glBindVertexArray(0)之间的所有对VBO和EBO的操作都会被VAO记录下来,那么当我们再次调用glBindVertexArray(VAO1)时,OpenGL就知道我们需要使用在VAO1中设置好的VBO和VEO。

在使用glVertexAttribPointer() 时,我们要注意里面的参数,主要是位置偏移。具体的请查看相关手册。

3.着色器

要使用着色器,我们必须创建我们自己的着色器对象,然后我们将着色器源码放放入着色器对象,编译它们;我们还需要运行着色器的着色器程序对象,并且将着色器对象放入着色器程序对象中,并链接对个着色器对象,当我们使用某个着色器程序对象时,OpenGL就会自动对 待绘制的图形使用之前放入的着色器对象,它将按照我们再着色器源码中写的那样渲染出图形。

//  声明一个GLuint 来存储顶点着色器对象的ID
GLuint vertexShader;// 创建一个着色器对象,类型是顶点着色器。片元着色器是 GL_FRAGEMENT_SHADER
vertexShader = glCreateShader(GL_VERTEX_SHADER);// 为顶点着色器对象绑定源码(字符串)
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);// 编译着色器
glCompileShader(vertexShader);// 输出编译错误信息
GLint success;
GLchar infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}elsestd::cout << "Compile VertexShader success" << std::endl;/*  创建并编译片元着色器
*/// 创建着色器程序对象
GLuint shaderProgram;
shaderProgram = glCreateProgram();// 为着色器程序对象附加着色器对象
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);// 链接着色器
glLinkProgram(shaderProgram);// 使用着色器,任何时候,只要我们需要,我们就可以调用这段代码
glUseProgram(shaderProgram);

上面的代码只列出了顶点着色器,片元着色器的创建方法类似。下面的代码是顶点着色器和偏远着色器的源码。

// shader1.vert 顶点着色器
#version 330 corelayout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
layout(location = 2) in vec2 texCoord;out vec3 ourColor;
out vec2 TexCoord;uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform mat4 trans;void main(){gl_Position = projection* view*model * vec4(position, 1.0f);ourColor = color;TexCoord = texCoord;
}// 片元着色器
#version 330 corein vec3 ourColor;
in vec2 TexCoord;out vec4 fragColor;// GL_TEXTURE0 的纹理采样器ID,只要指定了GL_TEXTURE0,OpenGL会自己生成这个变量
uniform sampler2D ourTexture1;
uniform sampler2D ourTexture2;// GL_TEXTURE1 的纹理采样器ID,同上uniform float angle;
uniform float opcity;void main()
{float a = angle * 3.1415926 / 180;vec2 TexCoord2 = vec2(cos(a) * TexCoord.x - sin(a)*TexCoord.y, TexCoord.x * sin(a) + TexCoord.y * cos(a));// 混合两个颜色fragColor = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord2), opcity);
}

主循环

 while (!glfwWindowShouldClose(window)) {GLfloat timeValue = (GLfloat)glfwGetTime();deltaTime = timeValue - lastFrame;lastFrame = timeValue;do_movement();glfwPollEvents();glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置默认颜色glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //其他还有深度缓冲,模板缓冲glEnable(GL_DEPTH_TEST);shader1.use();glBindVertexArray(VAO1);glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, (void*)0);glBindVertexArray(0);glfwSwapBuffers(window);        }

4. 使用纹理

纹理的使用我们需要使用某些库来辅助我们读取图片数据,毕竟谁也不想与种类繁多的图片格式打交道。我们可以使用SOIL库或者stb_image等,但他们的作用都只是帮助你更方便使用图片。这里使用的是SOIL。

很多时候我们需要先禁用4字节对齐,改为1字节对齐,因为OpenGL默认使用4字节对齐,而很多图片他们并没有完整的4通道或者其他原因导致他的数据不是4的整数倍,如果这时OpenGL按照4的倍数来计算图片的尺寸的话往往会存在些偏差,这些偏差表现在显示上就是图片会倾斜。

// 禁用4字节对齐
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // 使用SOIL库读取图片文件,并存储在image指针指向的区域
int widthImg, heightImg;
unsigned char* image = SOIL_load_image("container.png", &widthImg, &heightImg, 0, SOIL_LOAD_RGB);  // 启动第1个纹理单元,后面对纹理的操作将在TEXTURE0上进行
// 我们可以重复下面的操作继续往第1,2,3...16个纹理单元上设置数据,
// 这样后面我们只需要切换当前使用的纹理既可以获取之前设置的纹理数据
glActiveTexture(GL_TEXTURE0);//创建一个纹理对象
GLuint texture;
glGenTextures(1, &texture);// 并将GL_TEXTURE_2D 绑定的对象指定为该纹理对象,后面对GL_TEXTURE_2D的操作都是在这个纹理ID上
glBindTexture(GL_TEXTURE_2D, texture);//从图片数据中生成纹理,放到之前绑定的纹理对象中
//第二个参数时0表示只生成Mipmap中0级别的纹理,如果需要生成其他级别的修改0即可
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, widthImg, heightImg, 0, GL_RGB, GL_UNSIGNED_BYTE, image);// 删除之前从文件读取的图片数据,因为纹理数据已经使用了该图片数据
SOIL_free_image_data(image);//使用之前的纹理对象创建Mipmap,生成所有级别的Mipmap
glGenerateMipmap(GL_TEXTURE_2D);// 设置纹理边框
float borderColor[] = { 1.0f, 0.0f, 0.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);//环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);// 过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glBindTexture(GL_TEXTURE_2D, 0);//解绑// 转换纹理的操作完成后再开启4字节对齐,也可以在读取。设置纹理数据结束后就执行该操作
glPixelStorei(GL_UNPACK_ALIGNMENT, 4); /* 我们可以在主循环中这样使用 */// 启用着色器程序
shader1.use();   // 使用第1个纹理单元
// 因为上面创建纹理时绑定了一次,但是修改完纹理后马上解绑了,所以这里必须重新绑定
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(shader1.Program, "ourTexture1"), 0);// 使用第2个纹理单元
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texture1);
glUniform1i(glGetUniformLocation(shader1.Program, "ourTexture2"), 1);

5. 坐标系变换及摄像机移动

之前的OpenGL渲染管线之坐标系 (二)已经叙述过OpenGL之间的坐标系,上一节的顶点着色器中已经置入了模型矩阵、观察矩阵、投影矩阵。下面我只会记录一些学习过程中的额外内容。

// 创建模型矩阵,这里只是将所有模型统一往屏幕内部旋转了timeValue * 5度
glm::mat4 modeltemp;
modeltemp = glm::rotate(model, glm::radians(timeValue * 5), glm::vec3(0.5f, 1.0f, 0.0f));// 创建观察矩阵,由相机的位置,相机的视线聚焦处,以及相机向上的坐标轴即可确定摄像机坐标系
// 视线方向不与cameraUp垂直时,怎么办?
glm::mat4 view;
view = glm::lookAt(cameraPos, cameraFront + cameraPos, cameraUp);// 创建透视投影矩阵,将相机坐标系中的顶点转换到裁剪空间(平面,二维)
glm::mat4 projection;
projection = glm::perspective(glm::radians(aspect), (GLfloat)screenWidth / screenHeight, 0.1f, 100.0f);// 将MVP矩阵置入Shader
glUniformMatrix4fv(glGetUniformLocation(shader1.Program, "model"), 1, GL_FALSE, glm::value_ptr(modeltemp));
glUniformMatrix4fv(glGetUniformLocation(shader1.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(glGetUniformLocation(shader1.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

为了实现可控镜头,我们需要改变镜头的位置,视线方向,以及视角。这些由摄像机当前位置,摄像机视线聚焦位置,以及摄像机向上的坐标轴方向确定。

只改变摄像机位置和视点位置比较简单,这里不赘述。我们讨论如何改变摄像机角度,我们可以使用欧拉角中的俯仰角pitch、偏航角yaw以及桶滚角roll来描述摄像机当前的方向。这里我们使用另外一种方式,以摄像机向上仰视为例,当我们按下上键,摄像机慢慢向上仰视。

我们使用视线方向和摄像机头顶方向的轴,获取另一个坐标轴,将视点往Up轴方向移动一定距离,这样我们的Up轴将发生变化,我们再刷新Up轴,即可。注:我们可以对两个向量叉乘得到与这两条向量都垂直的向量,这样可方便根据两条坐标轴获取第三条坐标轴,计算得到的第三条坐标轴的方向遵循右手定则。

if (keys[GLFW_KEY_UP]) {glm::vec3 cameraRight = glm::normalize(glm::cross((cameraFront - cameraPos), cameraUp));cameraFront += cameraSpeed * cameraUp;cameraUp = glm::normalize(glm::cross(cameraRight, (cameraFront - cameraPos)));
}

另外我们也可以使用鼠标操作来完善整个摄像机变换的过程,文章开头的项目中已经完整实现。

OpenGL渲染管线之简单示例(五)相关推荐

  1. 用opengl编写一个简单的画图软件示例代码

    //用opengl编写一个简单的画图软件示例代码(存在闪烁问题) //本代码,抄写自一本教授opengl的书,可惜,里面的代码存在一些问题,导致不能正常显示,现在是增加了一些语句的代码 #includ ...

  2. 红宝书阅读笔记——OPENGL渲染管线

    之前读的时候一直觉得红宝书是很艰涩难懂的,不如NEHE的教程简单. 后来才发觉是自己没基础,几番折腾之后也只能用OPENGL做些简单的东西.半年没写,连glBegin都给忘了. 图形学的大作业要求写个 ...

  3. 小强学渲染之OpenGL渲染管线详析

    什么是OpenGL? OpenGL是一套图形硬件的软件API接口库,它直接和GPU交互,将3D场景渲染绘制到2D屏幕上.总结说,OpenGL的功能是将程序中定义的各种2D或3D模型绘制到帧缓存中,或者 ...

  4. 【Redis】三、Redis安装及简单示例

    (四)Redis安装及使用   Redis的安装比较简单,仍然和大多数的Apache开源软件一样,只需要下载,解压,配置环境变量即可.具体安装过程参考:菜鸟教程Redis安装.   安装完成后,通过r ...

  5. 《OpenGL编程指南(原书第9版)》——1.4 OpenGL渲染管线

    1.4 OpenGL渲染管线 OpenGL实现了我们通常所说的渲染管线(rendering pipeline),它是一系列数据处理过程,并且将应用程序的数据转换到最终渲染的图像.图1-2所示为Open ...

  6. php 遍历目录函数,PHP 遍历指定目录所有文件函数的简单示例(可指定文件类型)...

    这篇文章主要为大家详细介绍了PHP 遍历指定目录所有文件函数的简单示例(可指定文件类型),具有一定的参考价值,可以用来参考一下. 对PHP遍历指定目录下所有文件函数,可指定文件类型感兴趣的小伙伴,下面 ...

  7. php中调整图片大小,php 调整图片尺寸的简单示例

    这篇文章主要为大家详细介绍了php 调整图片尺寸的简单示例,具有一定的参考价值,可以用来参考一下. 对php调整图片尺寸的代码感兴趣的小伙伴,下面一起跟随512笔记的小编两巴掌来看看吧! /** * ...

  8. php简单抽奖,php 简单随机抽奖函数的简单示例

    这篇文章主要为大家详细介绍了php 简单随机抽奖函数的简单示例,具有一定的参考价值,可以用来参考一下. 对php编写的简单随机抽奖函数感兴趣的小伙伴,下面一起跟随512笔记的小编两巴掌来看看吧! /* ...

  9. 用OpenInventor实现的NeHe OpenGL教程-第二十五课

    用OpenInventor实现的NeHe OpenGL教程-第二十五课           NeHe教程在这节课中向我们介绍了如何从文件加载3D模型,并且平滑的从一个模型变换为另一个模型.两个模型之间 ...

最新文章

  1. 高等数学思维导图_直击高数重点!这份思维导图请收下
  2. openresty 安装
  3. SSH服务理论+实践
  4. DNS抓包分析--wireshark
  5. unsafe java_Java如何以及为什么使用Unsafe?
  6. 如何使用云原生数据湖,助力线上教育行业逐步智能化
  7. 使用Anaconda进行环境和包的管理
  8. 深度学习《自动编码器》
  9. nginx ci index.php,CI在Nginx服务器上rewrite去掉index.php例子
  10. 你知道高并发的性能测试怎么做吗?
  11. C语言中全局变量存放在哪个位置?
  12. glassfish启动后不能进入部署页面_使用Jenkins实现项目持续集成部署
  13. S3VM和TSVM的不同
  14. webpack 生产环境下插件用途
  15. 【转】【信息学奥赛一本通】题解目录
  16. QQ浏览器x5内核的兼容性问题
  17. 【文字识别】OCR截图文字识别提取(无需安装)拖拽图片,打开图片,图片PDF转文字的好帮手
  18. 单片机第三讲 ——中断及定时器基本知识
  19. 手机充值了还是显示无服务器,手机显示已联网,但却不能用,怎么办?
  20. 谷歌开源!一个格式化 Python 代码的好帮手!

热门文章

  1. 循环渐进NsDoor(一)
  2. 证书管理机构——CA(Certificate Authority)
  3. 编译原理学习笔记(一)
  4. 尺寸不会再乱 主板板型规格知识大解析
  5. 二次吐血整理的 MAYA教程 快捷键大全,别收藏,直接粘贴拿走!
  6. 南泰就业联盟Android,统一推送联盟终于发力!国产厂商纷纷加入:将彻底解决安卓卡顿问题...
  7. 嵌入式linux 更新源,openwrt如何修改为国内软件源
  8. centos 安装安全狗
  9. 【微信小程序】上传文件到阿里云OSS
  10. 单相干式变压器红外图像数据集