向 3D 世界迈出一小步
前言
OpenGL 的学习资料很多,一个是比较著名的 OpenGL 红宝书《OpenGL 编程指南》,可以在这里 http://opengl-redbook.com/ 下载该书配套的源代码;另一个是网络上的在线教程LearnOpenGL。所以,我这里就不再啰啰嗦嗦地介绍 OpenGL 的基础知识和计算机图形学的基础知识了,主要是简单阐述一些我自己的理解,以及写一些能跑起来、能看到效果的体验性的小程序。
通过前一篇阐述,可以看到在 Linux 系统中学习 OpenGL 是多么的方便。使用 GLEW 和 GLFW 库,使用一行简单的 g++ 编译命令,第一个简单的 OpenGL 程序就跑起来了。而上面提到的参考资料,都有让初学者头痛的地方,比如红宝书,它用到了比较少见的 gl3w 库,还用到了 cmake 系统,即使下载了它的源代码,程序也跑不起来。网络教程LearnOpenGL使用的是 GLAD,反正我没有使用成功过。最终,还是 GLEW 和 GLFW 搭配是最顺手的。
前一篇的最后,我们跑起来了一个空的 OpenGL 程序,里面没有渲染任何内容。在这一篇中,我们将向 3D 世界迈出一小步,了解 OpenGL 渲染管线的基本知识,并绘制一些简单的 3D 内容。
我所理解的简单的 3D 图形学知识
要想描述一个 3D 世界,最简单的办法就是描述一些顶点的坐标,(当然还有其它的办法,比如通过数学公式描述的曲线曲面之类的,所以我说描述顶点坐标是最简单的办法。),然后由顶点连接成三角形,再由三角形连接成面,最后,计算机再把这些数据处理成屏幕上像素的颜色,显示成我们看到的图像。
而 OpenGL 通过渲染管线完成这个操作,渲染管线由许多个阶段组成,首先,OpenGL 把我们传递给它的顶点进行坐标变换,把 3D 的坐标对应成屏幕上的点,然后,把这些顶点组装成三角形,然后,对这些三角形内部的像素进行插值,生成片元,最后,对这些片元进行计算,以生成在屏幕上显示的图像。很显然,渲染管线需要使用 GPU 进行加速。
GPU 是典型的众人拾柴火焰高的并行计算架构,它可以同时对成千上万的顶点进行计算,也可以同时对成千上万的片元进行计算。我的这台 XPS 9570,使用的显卡是 GTX 1050Ti,它有 768 个 CUDA 核心,我准备等有钱了再上一个 RTX 3060 显卡的笔记本,据说有 3840 个 CUDA 核心,性能可以一下子提升 5 倍, _。让这些核心运行什么样的任务,是由我们指定的,也就是说,我们要写一些小程序,然后把这些小程序传递给 GPU 核心执行,这些小程序就叫做 Shader。在 OpenGL 的渲染管线中,每一个阶段都对应一个 Shader。
通过上面的描述,我们的工作流程呼之欲出。要想使用 OpenGL 渲染一点有用的东西,我们需要做下面这些工作:1.准备一些顶点数据;2.写一些 Shader 程序,并组装成渲染管线;3.把顶点数据传递给 Shader,让 Shader 处理。
特别需要注意的还有两点:1. 顶点数据可以不仅仅只是坐标,还可以包含法线呀、颜色值呀、纹理坐标呀等数据,还可以包含其它任何我们自定义的数据;2. Shader 是并行执行的,每个 Shader 处理一个顶点的数据,不同的顶点其传入的数据是不同的,可以认为这些数据是逐顶点变量,但是,也可以给所有的 Shader 传递一些统一的值,这些值在所有的 Shader 中是相同的,称之为 Uniform 变量。
这些数据,通过一系列不同的 OpenGL 函数传递给 Shader。
下面开始实战。
准备顶点数据
顶点数据怎么来,这是一个问题。我们可以直接手写三个顶点坐标,在屏幕上渲染一个三角形,同样的方法,也可以手写八个顶点坐标及其颜色值渲染一个颜色立方体。对于一些更复杂的几何图形,比如球体、甜甜圈这样的几何体,只要会一点简单的三角函数,我们自己也可以生成出来。另外,对于一些复杂的、经典的模型,也可以找到它们的出处。比如红宝书中就有一个 armadillo 神兽的 3D 模型,可以在红宝书源代码的github仓库中找到,只不过是作者自创的 vbm 格式,需要自己动手扒一下。再比如 freeglut 中经典的茶壶模型,也可以在freeglut的github仓库中找到,只不过它使用的是贝塞尔曲面的描述方式,这个技术含量就高了那么一点点了。
对于顶点数据的每一个分量(如坐标分量、法向量分量、纹理坐标分量、颜色分量等),我们都可以使用 GLM 库中的vec2、vec3、vec4格式表示,然后组织成 struct,再然后组织成 vector,这是很自然的事情。如果使用到顶点索引,则需要再准备一个索引数组的 vector。再然后,就是 OpenGL 的那一套 API 了,创建 VAO、VBO,如果有索引,再创建 EBO,然后绑定对象,创建 Buffer,绑定 Buffer,向 Buffer 中存入数据,最后使用 DrawArrays() 进行渲染,如果使用到了顶点索引,则使用 DrawElements() 进行渲染。
在这里,我先创建一个 Mesh 类用来对以上流程进行一个封装,并创建 Mesh 类的几个子类,分别生成平面、球体、甜甜圈这几种基本图形,每个顶点具有坐标、法向量、纹理坐标(只有1组纹理坐标)。我把这些结构和类都放到一个文件 mesh.hpp 中,内容如下:
#ifndef __MESH_H__
#define __MESH_H__
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <vector>
#include <string>
#include <GL/glew.h>
struct Vertex{
glm::vec4 position;
glm::vec3 normal;
glm::vec2 texCoord;
};
class Mesh{
protected:
std::vector<Vertex> vertices;
std::vector<GLuint> indices;
GLuint VAO, VBO, EBO;
public:
void generateMesh(int iSlices);
void setup(){
glCreateVertexArrays(1, &VAO);
glBindVertexArray(VAO);
glCreateBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glNamedBufferStorage(VBO, sizeof(Vertex)*vertices.size(), &vertices[0], 0);
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(float)*indices.size(), &indices[0], GL_STATIC_DRAW);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
glEnableVertexAttribArray(2);
}
void render(){
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
}
};
class Plane: public Mesh{
public:
void generateMesh(int iSlices){
int n = iSlices + 1;
float s = 2.0f/(float)iSlices;
Vertex temp_vertex;
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
temp_vertex.position = glm::vec4(s*j - 1.0f, s*i - 1.0f, 0.0f, 1.0f);
temp_vertex.normal = glm::vec3(0.0f, 0.0f, 1.0f);
temp_vertex.texCoord = glm::vec2(1.0f/(float)iSlices * j, 1.0f/(float)iSlices * i);
vertices.push_back(temp_vertex);
}
}
for(int i=0; i<iSlices; i++){
for(int j=0; j<iSlices; j++){
indices.push_back(i*n + j);
indices.push_back((i+1)*n + j+1);
indices.push_back((i+1)*n + j);
indices.push_back(i*n + j);
indices.push_back(i*n + j + 1);
indices.push_back((i+1)*n + j+1);
}
}
}
};
class Sphere: public Mesh{
public:
void generateMesh(int iSlices){
int m = iSlices/2 + 1;
int n = iSlices+1;
float s = 360.0f/(float)iSlices;
glm::vec4 up(0.0f, 1.0f, 0.0f, 1.0f);
glm::mat4 I(1.0f);
glm::vec3 X(1.0f, 0.0f, 0.0f);
glm::vec3 Y(0.0f, 1.0f, 0.0f);
glm::vec3 Z(0.0f, 0.0f, 1.0f);
Vertex temp_vertex;
for(int i=0; i<m; i++){
for(int j=0; j<n; j++){
temp_vertex.position = glm::rotate(I, glm::radians(s*j), Y) * glm::rotate(I, glm::radians(s*i), Z) * up;
temp_vertex.normal = temp_vertex.position;
temp_vertex.texCoord = glm::vec2(1.0f/(float)(n-1) * j, 1.0f/(float)(m-1) * i);
vertices.push_back(temp_vertex);
}
}
for(int i=0; i<m-1; i++){
for(int j=0; j<n-1; j++){
indices.push_back(i*n + j);
indices.push_back((i+1)*n + j);
indices.push_back((i+1)*n + j+1);
indices.push_back(i*n + j);
indices.push_back((i+1)*n + j+1);
indices.push_back(i*n + j + 1);
}
}
}
};
class Torus: public Mesh{
public:
void generateMesh(int iSlices){
int n = iSlices + 1;
float s = -360.0f/(float)iSlices;
glm::vec4 top(0.0f, 0.2f, 0.0f, 1.0f);
glm::vec4 normal_up(0.0f, 1.0f, 0.0f, 1.0f);
glm::mat4 I(1.0f);
glm::vec3 X(1.0f, 0.0f, 0.0f);
glm::vec3 Y(0.0f, 1.0f, 0.0f);
glm::vec3 Z(0.0f, 0.0f, 1.0f);
Vertex temp_vertex;
for(int i=0; i<n; i++){
for(int j=0; j<n; j++){
temp_vertex.position = glm::rotate(I, glm::radians(s*j), Z)
* glm::translate(I, glm::vec3(0.0f, 0.8f, 0.0f))
* glm::rotate(I, glm::radians(s*i), X)
* top;
temp_vertex.normal = glm::rotate(I, glm::radians(s*j), Z)
* glm::rotate(I, glm::radians(s*i), X)
* normal_up;
temp_vertex.texCoord = glm::vec2(1.0f/(float)iSlices * j * 4, 1.0f/(float)iSlices * i);
vertices.push_back(temp_vertex);
}
}
for(int i=0; i<iSlices; i++){
for(int j=0; j<iSlices; j++){
indices.push_back(i*n + j);
indices.push_back((i+1)*n + j+1);
indices.push_back((i+1)*n + j);
indices.push_back(i*n + j);
indices.push_back(i*n + j + 1);
indices.push_back((i+1)*n + j+1);
}
}
}
};
#endif
编译和连接 Shader
在 OpenGL 中编译和连接 Shader 的流程是固定的,也就是那几个 API,详细的知识我就不赘述了,基本所有的教材都有,对于简单的程序来说,一个 Vertex Shader 和一个 Fragment Shader 就可以了,至于几何着色器、细分着色器这样的高级知识,等用到的时候再说。关于着色器的编译连接,我也写了一个 Shader 类对它进行了封装,放到了文件 shader.hpp 中,如下:
#ifndef __SHADER_HPP__
#define __SHADER_HPP__
#include <string>
#include <iostream>
#include <fstream>
#include <sstream>
#include <GL/glew.h>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
struct ShaderInfo{
GLenum type;
std::string filename;
GLuint shader_id;
};
class Shader{
private:
GLuint program_id;
public:
Shader(){
program_id = 0;
}
Shader(ShaderInfo* shaders){
if(shaders == nullptr){
program_id = 0;
return;
}
program_id = glCreateProgram();
ShaderInfo* entry = shaders;
//加载并编译Shader
while(entry->type != GL_NONE){
GLuint shader_id = glCreateShader(entry->type);
entry->shader_id = shader_id;
std::ifstream fs(entry->filename);
std::string content((std::istreambuf_iterator<char>(fs)), std::istreambuf_iterator<char>());
if(content.empty()){ //只要有一个Shader文件打不开,就删掉之前创建的所有Shader
std::cerr << "Unable to open file '" << entry->filename << "'" << std::endl;
for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
glDeleteShader( entry->shader_id );
entry->shader_id = 0;
}
return;
}
const GLchar* source = content.c_str();
glShaderSource( shader_id, 1, &source, NULL );
glCompileShader( shader_id );
GLint compiled;
glGetShaderiv( shader_id, GL_COMPILE_STATUS, &compiled );
if ( !compiled ) {//如果Shader编译失败,输出失败原因,便于调试
GLsizei len;
glGetShaderiv( shader_id, GL_INFO_LOG_LENGTH, &len );
GLchar* log = new GLchar[len+1];
glGetShaderInfoLog( shader_id, len, &len, log );
std::cerr << entry->filename << "," << "Shader compilation failed: " << log << std::endl;
delete [] log;
//编译失败,也要删除前面创建的所有Shader
for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
glDeleteShader( entry->shader_id );
entry->shader_id = 0;
}
return ;
}
glAttachShader( program_id, shader_id );
++entry;
}
//进入连接阶段
glLinkProgram( program_id );
GLint linked;
glGetProgramiv( program_id, GL_LINK_STATUS, &linked );
if ( !linked ) {//如果连接失败,则输出调试信息,同样删除之前创建的所有Shader
GLsizei len;
glGetProgramiv( program_id, GL_INFO_LOG_LENGTH, &len );
GLchar* log = new GLchar[len+1];
glGetProgramInfoLog( program_id, len, &len, log );
std::cerr << "Shader linking failed: " << log << std::endl;
delete [] log;
for ( entry = shaders; entry->type != GL_NONE; ++entry ) {
glDeleteShader( entry->shader_id );
entry->shader_id = 0;
}
return;
}
}
GLuint getId(){
return program_id;
}
void setCurrent(){
glUseProgram(program_id);
}
void setModelMatrix(glm::mat4 model_matrix){
glUniformMatrix4fv(glGetUniformLocation(program_id, "model_matrix"), 1, GL_FALSE, glm::value_ptr(model_matrix));
}
void setViewMatrix(glm::mat4 view_matrix){
glUniformMatrix4fv(glGetUniformLocation(program_id, "view_matrix"), 1, GL_FALSE, glm::value_ptr(view_matrix));
}
void setProjectionMatrix(glm::mat4 projection_matrix){
glUniformMatrix4fv(glGetUniformLocation(program_id, "projection_matrix"), 1, GL_FALSE, glm::value_ptr(projection_matrix));
}
};
#endif
该 Shader 类还提供了一些向 Shader 中传递 Uniform 变量的方法,最常见的 Uniform 变量就是模型矩阵、视图矩阵和投影矩阵。
主程序
主程序文件为 SphereWorld.cpp,在该文件中,创建一个 App 类的字类,并在 init 方法中创建一个平面、一个球体、一个甜甜圈,并使用模型矩阵分别将它们进行平移、缩放和旋转,以放到场景中的适当位置,另外,使用视图矩阵将 Camera 放到适当的位置,最后,创建合适的透视投影矩阵,就可以看到非常逼真的三维图像了。在创建和使用这些矩阵的时候,GLM 为我们提供了非常大的方便。目前还没有涉及到光照和纹理,故将 OpenGL 设置为线框模式,这样看起来更加立体。程序代码如下:
#include "../include/app.hpp"
#include "../include/shader.hpp"
#include "../include/mesh.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class MyApp : public App {
private:
const GLfloat clearColor[4] = {0.2f, 0.3f, 0.3f, 1.0f};
Plane plane;
Sphere sphere;
Torus torus;
Shader* shaderSphereWorld;
public:
void init(){
ShaderInfo shaders[] = {
{GL_VERTEX_SHADER, "sphereworld.vert"},
{GL_FRAGMENT_SHADER, "sphereworld.frag"},
{GL_NONE, ""}
};
shaderSphereWorld = new Shader(shaders);
plane.generateMesh(20);
plane.setup();
sphere.generateMesh(60);
sphere.setup();
torus.generateMesh(60);
torus.setup();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
}
void display(){
glClearBufferfv(GL_COLOR, 0, clearColor);
glClear(GL_DEPTH_BUFFER_BIT);
glm::mat4 I(1.0f);
glm::vec3 X(1.0f, 0.0f, 0.0f);
glm::vec3 Y(0.0f, 1.0f, 0.0f);
glm::vec3 Z(0.0f, 0.0f, 1.0f);
float t = (float)glfwGetTime();
glm::mat4 view_matrix = glm::translate(I, glm::vec3(0.0f, 0.0f, -5.0f))
* glm::rotate(I, t, Y);
glm::mat4 projection_matrix = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 100.0f);
glm::mat4 plane_model_matrix = glm::translate(I, glm::vec3(0.0f, -1.0f, 0.0f))
* glm::rotate(I, glm::radians(-90.0f), X)
* glm::scale(I, glm::vec3(50.0f, 50.0f, 50.0f));
shaderSphereWorld->setModelMatrix(plane_model_matrix);
shaderSphereWorld->setViewMatrix(view_matrix);
shaderSphereWorld->setProjectionMatrix(projection_matrix);
shaderSphereWorld->setCurrent();
plane.render();
glm::mat4 sphere_model_matrix = glm::translate(I, glm::vec3(1.0f, 0.3f, 0.0f))
* glm::scale(I, glm::vec3(0.8f, 0.8f, 0.8f));
shaderSphereWorld->setModelMatrix(sphere_model_matrix);
sphere.render();
glm::mat4 torus_model_matrix = glm::translate(I, glm::vec3(-1.0f, 0.3f, 0.0f))
* glm::rotate(I, glm::radians(90.0f), Y)
* glm::scale(I, glm::vec3(1.3f, 1.3f, 1.3f));
shaderSphereWorld->setModelMatrix(torus_model_matrix);
torus.render();
}
~MyApp(){
if(shaderSphereWorld != NULL){
delete shaderSphereWorld;
}
}
};
DECLARE_MAIN(MyApp)
我们暂时还没有实现在场景中漫游的功能,所以只能让它自己旋转,以便于我们全方位地观察。在这里,是通过 glfwGetTime() 获取程序运行的时间,并根据时间值来生成视图矩阵来实现这个功能的。
编写 Shader 程序
这里的 Shader 程序是 sphereworld.vert 和 sphereworld.frag,前者是顶点着色器程序,后者是片元着色器程序。都很简单。
sphereworld.vert 文件的内容如下:
#version 460
uniform mat4 model_matrix;
uniform mat4 projection_matrix;
uniform mat4 view_matrix;
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 1) in vec2 vTexCoord;
out vec4 fColor;
out vec3 fNormal;
out vec2 fTexCoord;
out vec4 fPosition;
void main(void)
{
mat4 MV_matrix = view_matrix * model_matrix;
gl_Position = projection_matrix * view_matrix * model_matrix * vPosition;
fPosition = MV_matrix * vPosition;
fNormal = normalize(transpose(inverse(mat3(MV_matrix))) * vNormal);
fTexCoord = vTexCoord;
fColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
}
sphereworld.frag 文件内容如下:
#version 460
layout (location = 0) out vec4 color;
in vec4 fColor;
in vec3 fNormal;
in vec2 fTexCoord;
in vec4 fPosition;
void main(void)
{
color = fColor;
}
最终效果
编译,运行,就可以看到最终的效果了。如下图:

我使用的是 Visual Studio Code。从截图中可以但到 SphereWorld 这个实例的所有文件组成情况。好了,今天就到这里。
版权申明
该随笔由京山游侠在2021年02月07日发布于博客园,引用请注明出处,转载或出版请联系博主。QQ邮箱:1841079@qq.com
向 3D 世界迈出一小步的更多相关文章
- NeHe OpenGL教程 第十课:3D世界
转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...
- 第1部分: 游戏引擎介绍, 渲染和构造3D世界
原文作者:Jake Simpson译者: 向海Email:GameWorldChina@myway.com ---------------------------------------------- ...
- 【Stage3D学习笔记续】真正的3D世界(六):空间大战
这就是书上的最终效果了,一个完整的空间大战游戏: 点击查看源码 这里并没有太多的新知识,所涉及的东西更多的是游戏开发方面的优化和技巧,下面我们大家一起来看看: 飞船: 类似粒子效果中的粒子创建方法,我 ...
- 【Stage3D学习笔记续】真正的3D世界(二):显示模型
虽然我们进入真3D世界了,但是上一章的Demo仍然是显示的一个平面,尽管我们的平面在3D空间中旋转可以看出一点3D透视的效果,但是既然是真3D,就要拿出点3D的样子来! 如果要显示3D模型,我们就要告 ...
- 【Stage3D学习笔记续】真正的3D世界(一):透视矩阵
如果各位看官跟着我的学习笔记一路看过来的话,一定会吐槽我的,这都是什么3D啊?从头到尾整个都是在使用GPU绘制一堆2D图像而已,的确,之前我们一直使用正交矩阵利用GPU加速来实现2D世界的展示,算不上 ...
- 3d世界是怎样呈现到屏幕上的
要把一个3d物体呈现在屏幕上,要经过一系列的步骤. 描述3d世界 把3d世界绘制在二维屏幕上 如何描述一个3D世界? 数学家早就给出了3D世界的模型,我们日常最熟悉的3维坐标系就是一个欧几里得空间(线 ...
- 第10课 OpenGL 3D世界
加载3D世界,并在其中漫游: 在这一课中,你将学会如何加载3D世界,并在3D世界中漫游.这一课使用第一课的代码,当然在课程说明中我只介绍改变了代码. 这一课是由Lionel Brits (βtelge ...
- 3D 世界的钥匙「GitHub 热点速览 v.22.08」
有没有想过把身边的物件儿转成 3D 动画,在网页上实现一把?本期特推的项目 Three.js 就是帮你创建 3D 页面的知名开源项目,好玩的 3D 世界在向你招手.除了打开浏览器 3D 世界的钥匙外, ...
- 3D世界变换
一直弄不清3D场景中scaleOrientation的作用,还有scale.orientation(roation).translation的顺序问题,以往都是试图查一下,关于前者网上几乎找不到什么清 ...
随机推荐
- 【Linux】md5sum 生产所有文件的md5值,并对照目标文件是否相同
现在加入有很多很多文件需要测试md5,想看下是否都传输成功了,如何批量生成文件的md5并且逐条对照呢? 下面来简单介绍下 md5sum这个命令有一个选项"-c" 这个选项的意思是c ...
- ActiceMQ详解
1. MQ理解 1.1 MQ的产品种类和对比 MQ即消息中间件.MQ是一种理念,ActiveMQ是MQ的落地产品. 消息中间件产品 各类MQ对比 Kafka 编程语言:Scala 大数据领域的主流MQ ...
- [GKCTF2020]老八小超市儿
题目来自buu 一.题目初探 首先是一个shopxo搭建的演示站,通过扫描后台得到如下的网页 二.题目解答 首先是找到后台登陆的admin.php,然后通过百度找到shopxo的默认管理员登陆账号和密 ...
- ping 命令示例
将下面的代码粘贴到记事本中,然后保存为扩展名为BAT的文件,运行就可以将网段内ping不通的IP地址写入到文本文件IP.txt中. @echo offsetlocal ENABLEDELAYEDEXP ...
- java虚拟机入门(二)-探索内存世界
上节简单介绍了一下jvm的内存布局以及简单概念,那么对于虚拟机来说,它是怎么一步步的让我们能执行方法的呢: 1.首先,jvm启动时,跟个小领导一样会根据配置参数(没有配置的话jvm会有默认值)向大领导 ...
- 我为什么不鼓吹 WireGuard
原文链接:https://fuckcloudnative.io/posts/why-not-wireguard/ 最近有一款新型 VPN 工具备受瞩目,相信很多人已经听说过了,没错就是 WireGua ...
- SuperUpdate.sh 一键更换Linux软件源脚本
一.前言 有时候会遇到 Linux 的源更新速度非常的缓慢,特别是在国内使用默认的源,因为国内的网络环境,经常会出现无法更新,更新缓慢的情况.在这种情况下,更换一个更适合或者说更近,更快的软件源,会为 ...
- 提供个HDFS的目录的路径,对该目录进行创建和删除操作。创建目录时,如果目录 文件所在目录不存在则自动创建相应目录;删除目录时,由用户指定当该目录不为空时是否还删 除该目录
import java.io.IOException; import java.util.Scanner; import org.apache.hadoop.fs.*; public class G_ ...
- (Oracle)预定义异常
预定义异常: 为了 Oracle 开发和维护的方便,在 Oracle 异常中,为常见的异常码定义了对应的异常名称,称为预定义异常,常见的预定义异常有: 异常名称 异常码 描述 DUP_VAL_ON_I ...
- SpringMVC听课笔记(十四:异常处理)
1. SpringMVC通过HandlerExceptionResolver处理程序的异常,包括Handler映射,数据绑定以及目标方法执行时发生的异常 2.SpringMVC提供的HandlerEx ...