首页 > 其他 > 详细

openGL学习----相机

时间:2018-10-20 19:15:37      阅读:193      评论:0      收藏:0      [点我收藏+]

0.参考:https://learnopengl-cn.github.io/01%20Getting%20started/09%20Camera/

0.0其实相机就是搞清楚cameraPos,cameraFornt,cameraUp的关系和用法,以及跟三个欧拉角的关系,以及如何跟鼠标、键盘的wasd键联系起来(也就是视角移动跟距离移动)实现跟用户的交互,然后生成LookAt矩阵就OK了,重点是理解跟4中的注意点

0.1我们介绍的摄像机系统是一个FPS风格的摄像机,它能够满足大多数情况需要,而且与欧拉角兼容,但是在创建不同的摄像机系统,比如飞行模拟摄像机,时就要当心。每个摄像机系统都有自己的优点和不足,所以确保对它们进行了详细研究。比如,这个FPS摄像机不允许俯仰角大于90度,而且我们使用了一个固定的上向量(0, 1, 0),这在需要考虑滚转角的时候就不能用了。

0.2鼠标控制方向,键盘控制距离,滚轮控制视角

1.当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标(之前的原点是世界坐标系的原点):观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。细心的读者可能已经注意到我们实际上创建了一个三个单位轴相互垂直的、以摄像机的位置为原点的坐标系。

2.具体相机坐标系原理可参看资料。。。

3.这里只讲一下大致的观察矩阵的初始化api

相机坐标系转换就是生成一个观察矩阵,也就是著名的LookAt矩阵

技术分享图片

3.1幸运的是,GLM已经提供了这些支持。我们要做的只是定义一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量(我们计算右向量使用的那个上向量)。接着GLM就会创建一个LookAt矩阵,我们可以把它当作我们的观察矩阵:

glm::mat4 view;
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 3.0f), 
           glm::vec3(0.0f, 0.0f, 0.0f), 
           glm::vec3(0.0f, 1.0f, 0.0f));

glm::LookAt函数需要一个位置、目标和上向量。

其实比较经典和完美的初始化方法是下面(上面只是简单情况下的写法):

view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

 

第一个参数是相机位置,是个点,是世界坐标系,因为还没有生成观察矩阵作用到坐标上,怎么可能是相机坐标系呢

第二个参数是看向的目标位置,也是个点,也是世界坐标系。例子中是相机前面的点,也就是说相机一直是朝着它前面的目标点,也就是z轴负方向看的。它只是一个点,并不是一个向量,仅仅是表明在第一个参数的情况下,相机朝向哪个点看的(他与第一个参数一起才构成了相机的朝向向量,也就是朝向哪个方向看的)。

一般情况下这个点是由相机的点跟方向向量相加而得。因为已知他的方向向量(详见下面代码)

第三个参数是一个向量表示相机的上向量,指向y轴,可以通过这个向量跟前两个参数,两次向量叉乘出三个互相正交的向量

4.有了观察矩阵也就是都转换到相机坐标系了。这时候可以有一些交互设计了,比如鼠标控制朝向方向,键盘wasd控制移动,但是有些注意点(以下为相机移动的重点,要重点理解)

4.0主要讲解鼠标控制的视角移动跟键盘wasd控制的距离移动,以及滚轮控制的缩放移动

4.1键盘的wasd键用来控制前后左右的移动,其原理就是控制相机的位置,也就是更改cameraPos的值,那具体向哪个方向移动呢?对啦,就是向相机的方向向量(cameraFront)所指的方向移动就好啦,w就是他的方向,s就是反向,a就是他和相机的cameraUp向量叉乘出来的方向,d就是a的反向。但是要注意相乘的向量必须是单位向量,不然移动距离不对

技术分享图片

4.2关于移动速度:

目前我们的移动速度是个常量。理论上没什么问题,但是实际情况下根据处理器的能力不同,有些人可能会比其他人每秒绘制更多帧,也就是以更高的频率调用processInput函数。结果就是,根据配置的不同,有些人可能移动很快,而有些人会移动很慢。当你发布你的程序的时候,你必须确保它在所有硬件上移动速度都一样。

图形程序和游戏通常会跟踪一个时间差(Deltatime)变量,它储存了渲染上一帧所用的时间。我们把所有速度都去乘以deltaTime值。结果就是,如果我们的deltaTime很大,就意味着上一帧的渲染花费了更多时间,所以这一帧的速度需要变得更高来平衡渲染所花去的时间。使用这种方法时,无论你的电脑快还是慢,摄像机的速度都会相应平衡,这样每个用户的体验就都一样了。

4.3关于视角移动:

一共有3种欧拉角:俯仰角(Pitch)、偏航角(Yaw)和滚转角(Roll),下面的图片展示了它们的含义:

技术分享图片

欧拉角与相机方向向量的关系:

direction.x = cos(glm::radians(pitch)) * cos(glm::radians(yaw)); 
direction.y = sin(glm::radians(pitch));
direction.z = cos(glm::radians(pitch)) * sin(glm::radians(yaw));

具体坐标系再具体分析,鼠标动一下,欧拉角响应加或者减一些,相机向量cameraFornt变一下,但是十分注意三个欧拉角的初始值,并不一定全是0,有可能是-90度

技术分享图片
 1 void mouse_callback(GLFWwindow* window, double xpos, double ypos)
 2 {
 3     if (firstMouse)
 4     {
 5         lastX = xpos;
 6         lastY = ypos;
 7         firstMouse = false;
 8     }
 9 
10     float xoffset = xpos - lastX;
11     float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
12     lastX = xpos;
13     lastY = ypos;
14 
15     float sensitivity = 0.1f; // change this value to your liking
16     xoffset *= sensitivity;
17     yoffset *= sensitivity;
18 
19     yaw += xoffset;
20     pitch += yoffset;
21 
22     // make sure that when pitch is out of bounds, screen doesn‘t get flipped
23     if (pitch > 89.0f)
24         pitch = 89.0f;
25     if (pitch < -89.0f)
26         pitch = -89.0f;
27 
28     glm::vec3 front;
29     front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
30     front.y = sin(glm::radians(pitch));
31     front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
32     cameraFront = glm::normalize(front);
33 }
View Code

4.4还要注意一般不用翻滚角,且俯仰角的范围有限制,在正负89度之间,另外还有第一次捕捉鼠标位置时的问题

4.5鼠标滚轮的缩放移动

技术分享图片
 1 void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
 2 {
 3   if(fov >= 1.0f && fov <= 45.0f)
 4     fov -= yoffset;
 5   if(fov <= 1.0f)
 6     fov = 1.0f;
 7   if(fov >= 45.0f)
 8     fov = 45.0f;
 9 }
10 //fov是init投影矩阵的视角参数
View Code

5.完整的一个代码

技术分享图片
  1 #include <glad/glad.h>
  2 #include <GLFW/glfw3.h>
  3 #include <stb_image.h>
  4 
  5 #include <glm/glm.hpp>
  6 #include <glm/gtc/matrix_transform.hpp>
  7 #include <glm/gtc/type_ptr.hpp>
  8 
  9 #include <learnopengl/shader_m.h>
 10 
 11 #include <iostream>
 12 
 13 void framebuffer_size_callback(GLFWwindow* window, int width, int height);
 14 void mouse_callback(GLFWwindow* window, double xpos, double ypos);
 15 void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
 16 void processInput(GLFWwindow *window);
 17 
 18 // settings
 19 const unsigned int SCR_WIDTH = 800;
 20 const unsigned int SCR_HEIGHT = 600;
 21 
 22 // camera
 23 glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f);
 24 glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
 25 glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
 26 
 27 bool firstMouse = true;
 28 float yaw = -90.0f;    // yaw is initialized to -90.0 degrees since a yaw of 0.0 results in a direction vector pointing to the right so we initially rotate a bit to the left.
 29 float pitch = 0.0f;
 30 float lastX = 800.0f / 2.0;
 31 float lastY = 600.0f / 2.0;
 32 float fov = 45.0f;
 33 
 34 // timing
 35 float deltaTime = 0.0f;    // time between current frame and last frame
 36 float lastFrame = 0.0f;
 37 
 38 int main()
 39 {
 40     // glfw: initialize and configure
 41     // ------------------------------
 42     glfwInit();
 43     glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
 44     glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
 45     glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
 46 
 47 #ifdef __APPLE__
 48     glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
 49 #endif
 50 
 51     // glfw window creation
 52     // --------------------
 53     GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
 54     if (window == NULL)
 55     {
 56         std::cout << "Failed to create GLFW window" << std::endl;
 57         glfwTerminate();
 58         return -1;
 59     }
 60     glfwMakeContextCurrent(window);
 61     glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
 62     glfwSetCursorPosCallback(window, mouse_callback);
 63     glfwSetScrollCallback(window, scroll_callback);
 64 
 65     // tell GLFW to capture our mouse
 66     glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
 67 
 68     // glad: load all OpenGL function pointers
 69     // ---------------------------------------
 70     if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
 71     {
 72         std::cout << "Failed to initialize GLAD" << std::endl;
 73         return -1;
 74     }
 75 
 76     // configure global opengl state
 77     // -----------------------------
 78     glEnable(GL_DEPTH_TEST);
 79 
 80     // build and compile our shader zprogram
 81     // ------------------------------------
 82     Shader ourShader("6.2.coordinate_systems.vs", "6.2.coordinate_systems.fs");
 83 
 84     // set up vertex data (and buffer(s)) and configure vertex attributes
 85     // ------------------------------------------------------------------
 86     float vertices[] = {
 87         -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
 88          0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
 89          0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
 90          0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
 91         -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
 92         -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
 93 
 94         -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
 95          0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
 96          0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
 97          0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
 98         -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
 99         -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
100 
101         -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
102         -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
103         -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
104         -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
105         -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
106         -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
107 
108          0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
109          0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
110          0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
111          0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
112          0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
113          0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
114 
115         -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
116          0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
117          0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
118          0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
119         -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
120         -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
121 
122         -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
123          0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
124          0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
125          0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
126         -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
127         -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
128     };
129     // world space positions of our cubes
130     glm::vec3 cubePositions[] = {
131         glm::vec3(0.0f,  0.0f,  0.0f),
132         glm::vec3(2.0f,  5.0f, -15.0f),
133         glm::vec3(-1.5f, -2.2f, -2.5f),
134         glm::vec3(-3.8f, -2.0f, -12.3f),
135         glm::vec3(2.4f, -0.4f, -3.5f),
136         glm::vec3(-1.7f,  3.0f, -7.5f),
137         glm::vec3(1.3f, -2.0f, -2.5f),
138         glm::vec3(1.5f,  2.0f, -2.5f),
139         glm::vec3(1.5f,  0.2f, -1.5f),
140         glm::vec3(-1.3f,  1.0f, -1.5f)
141     };
142     unsigned int VBO, VAO;
143     glGenVertexArrays(1, &VAO);
144     glGenBuffers(1, &VBO);
145 
146     glBindVertexArray(VAO);
147 
148     glBindBuffer(GL_ARRAY_BUFFER, VBO);
149     glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
150 
151     // position attribute
152     glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
153     glEnableVertexAttribArray(0);
154     // texture coord attribute
155     glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
156     glEnableVertexAttribArray(1);
157 
158 
159     // load and create a texture 
160     // -------------------------
161     unsigned int texture1, texture2;
162     // texture 1
163     // ---------
164     glGenTextures(1, &texture1);
165     glBindTexture(GL_TEXTURE_2D, texture1);
166     // set the texture wrapping parameters
167     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
168     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
169     // set texture filtering parameters
170     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
171     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
172     // load image, create texture and generate mipmaps
173     int width, height, nrChannels;
174     stbi_set_flip_vertically_on_load(true); // tell stb_image.h to flip loaded texture‘s on the y-axis.
175     unsigned char *data = stbi_load("resources/textures/container.jpg", &width, &height, &nrChannels, 0);
176     if (data)
177     {
178         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
179         glGenerateMipmap(GL_TEXTURE_2D);
180     }
181     else
182     {
183         std::cout << "Failed to load texture" << std::endl;
184     }
185     stbi_image_free(data);
186     // texture 2
187     // ---------
188     glGenTextures(1, &texture2);
189     glBindTexture(GL_TEXTURE_2D, texture2);
190     // set the texture wrapping parameters
191     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
192     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
193     // set texture filtering parameters
194     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
195     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
196     // load image, create texture and generate mipmaps
197     data = stbi_load("resources/textures/awesomeface.png", &width, &height, &nrChannels, 0);
198     if (data)
199     {
200         // note that the awesomeface.png has transparency and thus an alpha channel, so make sure to tell OpenGL the data type is of GL_RGBA
201         glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
202         glGenerateMipmap(GL_TEXTURE_2D);
203     }
204     else
205     {
206         std::cout << "Failed to load texture" << std::endl;
207     }
208     stbi_image_free(data);
209 
210     // tell opengl for each sampler to which texture unit it belongs to (only has to be done once)
211     // -------------------------------------------------------------------------------------------
212     ourShader.use();
213     ourShader.setInt("texture1", 0);
214     ourShader.setInt("texture2", 1);
215 
216 
217     // render loop
218     // -----------
219     while (!glfwWindowShouldClose(window))
220     {
221         // per-frame time logic
222         // --------------------
223         float currentFrame = glfwGetTime();
224         deltaTime = currentFrame - lastFrame;
225         lastFrame = currentFrame;
226 
227         // input
228         // -----
229         processInput(window);
230 
231         // render
232         // ------
233         glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
234         glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
235 
236         // bind textures on corresponding texture units
237         glActiveTexture(GL_TEXTURE0);
238         glBindTexture(GL_TEXTURE_2D, texture1);
239         glActiveTexture(GL_TEXTURE1);
240         glBindTexture(GL_TEXTURE_2D, texture2);
241 
242         // activate shader
243         ourShader.use();
244 
245         // pass projection matrix to shader (note that in this case it could change every frame)
246         glm::mat4 projection = glm::perspective(glm::radians(fov), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
247         ourShader.setMat4("projection", projection);
248 
249         // camera/view transformation
250         glm::mat4 view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
251         ourShader.setMat4("view", view);
252 
253         // render boxes
254         glBindVertexArray(VAO);
255         for (unsigned int i = 0; i < 10; i++)
256         {
257             // calculate the model matrix for each object and pass it to shader before drawing
258             glm::mat4 model;
259             model = glm::translate(model, cubePositions[i]);
260             float angle = 20.0f * i;
261             model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
262             ourShader.setMat4("model", model);
263 
264             glDrawArrays(GL_TRIANGLES, 0, 36);
265         }
266 
267         // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
268         // -------------------------------------------------------------------------------
269         glfwSwapBuffers(window);
270         glfwPollEvents();
271     }
272 
273     // optional: de-allocate all resources once they‘ve outlived their purpose:
274     // ------------------------------------------------------------------------
275     glDeleteVertexArrays(1, &VAO);
276     glDeleteBuffers(1, &VBO);
277 
278     // glfw: terminate, clearing all previously allocated GLFW resources.
279     // ------------------------------------------------------------------
280     glfwTerminate();
281     return 0;
282 }
283 
284 // process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
285 // ---------------------------------------------------------------------------------------------------------
286 void processInput(GLFWwindow *window)
287 {
288     if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
289         glfwSetWindowShouldClose(window, true);
290 
291     float cameraSpeed = 2.5 * deltaTime;
292     if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
293         cameraPos += cameraSpeed * cameraFront;
294     if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
295         cameraPos -= cameraSpeed * cameraFront;
296     if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
297         cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
298     if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
299         cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed;
300 }
301 
302 // glfw: whenever the window size changed (by OS or user resize) this callback function executes
303 // ---------------------------------------------------------------------------------------------
304 void framebuffer_size_callback(GLFWwindow* window, int width, int height)
305 {
306     // make sure the viewport matches the new window dimensions; note that width and 
307     // height will be significantly larger than specified on retina displays.
308     glViewport(0, 0, width, height);
309 }
310 
311 // glfw: whenever the mouse moves, this callback is called
312 // -------------------------------------------------------
313 void mouse_callback(GLFWwindow* window, double xpos, double ypos)
314 {
315     if (firstMouse)
316     {
317         lastX = xpos;
318         lastY = ypos;
319         firstMouse = false;
320     }
321 
322     float xoffset = xpos - lastX;
323     float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top
324     lastX = xpos;
325     lastY = ypos;
326 
327     float sensitivity = 0.1f; // change this value to your liking
328     xoffset *= sensitivity;
329     yoffset *= sensitivity;
330 
331     yaw += xoffset;
332     pitch += yoffset;
333 
334     // make sure that when pitch is out of bounds, screen doesn‘t get flipped
335     if (pitch > 89.0f)
336         pitch = 89.0f;
337     if (pitch < -89.0f)
338         pitch = -89.0f;
339 
340     glm::vec3 front;
341     front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
342     front.y = sin(glm::radians(pitch));
343     front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
344     cameraFront = glm::normalize(front);
345 }
346 
347 // glfw: whenever the mouse scroll wheel scrolls, this callback is called
348 // ----------------------------------------------------------------------
349 void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
350 {
351     if (fov >= 1.0f && fov <= 45.0f)
352         fov -= yoffset;
353     if (fov <= 1.0f)
354         fov = 1.0f;
355     if (fov >= 45.0f)
356         fov = 45.0f;
357 }
View Code

 

openGL学习----相机

原文:https://www.cnblogs.com/guopinghai/p/9821327.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!