基于C++的OpenGL 06 之摄像机
2022/8/5 1:22:55
本文主要是介绍基于C++的OpenGL 06 之摄像机,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
1. 引言
本文基于C++语言,描述OpenGL的摄像机
前置知识可参考:
- 基于C++的OpenGL 05 之坐标系统 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)
笔者这里不过多描述每个名词、函数和细节,更详细的文档可以参考:
- 摄像机 - LearnOpenGL CN (learnopengl-cn.github.io)
2. 概述
OpenGL的坐标变换流程图如下:
有图可知:
- 摄像机的参数(如,位置、视点、方向)决定视图
根据变化的相对性,控制摄像机的参数可以看成物体的变化(如,摄像机后移相当于物体后移)
观察矩阵可由摄像机的位置、视点和方向计算,如下图:
计算公式:
\[LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \\ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \\ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \\ 0 & 1 & 0 & -\color{purple}{P_y} \\ 0 & 0 & 1 & -\color{purple}{P_z} \\ 0 & 0 & 0 & 1 \end{bmatrix} \]其中R是右向量,U是上向量,D是方向向量,P是摄像机位置向量;
位置向量是相反的,因为我们最终希望把世界平移到与我们自身移动的相反方向
3. 编码
控制摄像机的参数实质就是控制观察矩阵(view)
生成一个观察矩阵需要位置、视点和方向向量,GLM的lookAt()
函数可用于生成观察矩阵:
glm::mat4 view = glm::mat4(1.0f); 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::mat4 view = glm::mat4(1.0f); float radius = 10.0f; float camX = sin(glfwGetTime()) * radius; float camZ = cos(glfwGetTime()) * radius; view = glm::lookAt(glm::vec3(camX, 0.0f, camZ), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
运行一下,结果图如下:
4. 自由移动
控制摄像机的位置可以实现视角的前后左右上下移动
摄像机参数:
glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f);
观察矩阵:
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
使用按键WSAD实现前后左右移动:
void processInput(GLFWwindow *window) { ... float cameraSpeed = 0.05f; // adjust accordingly if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) cameraPos += cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) cameraPos -= cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; }
5. 视角移动
根据鼠标变化计算视角变化
float lastX = 0.0f, lastY = 0.0f; bool firstMouse = true; // 鼠标变化 void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if(firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; lastX = xpos; lastY = ypos; float sensitivity = 0.05; xoffset *= sensitivity; yoffset *= sensitivity; yaw += xoffset; pitch += yoffset; if(pitch > 89.0f) pitch = 89.0f; if(pitch < -89.0f) pitch = -89.0f; glm::vec3 front; front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); front.y = sin(glm::radians(pitch)); front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); cameraFront = glm::normalize(front); }
6. 滚轮缩放
根据透视投影的视角大小实现物体的缩放
float fov = 30.0f; // 鼠标滚轮变化 void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { if(fov >= 1.0f && fov <= 45.0f) fov -= yoffset; if(fov <= 1.0f) fov = 1.0f; if(fov >= 45.0f) fov = 45.0f; } ... // 投影矩阵 projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f);
7. 完整代码
按照上述步骤,如果顺利的话,已经实现了按键WSAD的移动、鼠标的视角移动和滚轮缩放
主要文件test.cpp
:
#include <glad/glad.h> #include <GLFW/glfw3.h> #include <iostream> #include <math.h> #include "Shader.hpp" #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #include <glm/glm.hpp> #include <glm/ext/matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale #include <glm/ext/matrix_clip_space.hpp> // glm::perspective #include <glm/gtc/type_ptr.hpp> //全局变量 glm::vec3 cameraPos = glm::vec3(0.0f, 0.0f, 3.0f); glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f); glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f); float lastX = 400.0f, lastY = 300.0f, yaw = -90.0f, pitch = 0.0f, fov = 30.0f; bool firstMouse = true; // 函数声明 void framebuffer_size_callback(GLFWwindow *window, int width, int height); void process_input(GLFWwindow *window); unsigned int *renderInit(); void render(unsigned int shaderProgram, unsigned int VAO, unsigned int texture1, unsigned int texture2); bool checkCompile(unsigned int shader); bool checkProgram(unsigned int shaderProgram); void mouse_callback(GLFWwindow* window, double xpos, double ypos); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); int main() { glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); GLFWwindow *window = glfwCreateWindow(800, 600, "CoordinateSystem", nullptr, nullptr); if (window == nullptr) { std::cout << "Faild to create window" << std::endl; glfwTerminate(); } glfwMakeContextCurrent(window); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Faild to initialize glad" << std::endl; return -1; } glad_glViewport(0, 0, 800, 600); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); glfwSetCursorPosCallback(window, mouse_callback); glfwSetScrollCallback(window, scroll_callback); unsigned int *arr = renderInit(); while (!glfwWindowShouldClose(window)) { process_input(window); // render std::cout << arr[0] << " " << arr[1] << " " << arr[2] << " " << arr[3] << " " << arr[4] << std::endl; render(arr[0], arr[1], arr[3], arr[4]); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteProgram(arr[0]); glDeleteVertexArrays(1, &arr[1]); glDeleteBuffers(1, &arr[2]); glfwTerminate(); return 0; } void framebuffer_size_callback(GLFWwindow *window, int width, int height) { glViewport(0, 0, width, height); } void process_input(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { glfwSetWindowShouldClose(window, true); } float cameraSpeed = 0.05f; // adjust accordingly if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) cameraPos += cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) cameraPos -= cameraSpeed * cameraFront; if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) cameraPos -= glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) cameraPos += glm::normalize(glm::cross(cameraFront, cameraUp)) * cameraSpeed; } // 鼠标变化 void mouse_callback(GLFWwindow* window, double xpos, double ypos) { if(firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; lastX = xpos; lastY = ypos; float sensitivity = 0.05; xoffset *= sensitivity; yoffset *= sensitivity; yaw += xoffset; pitch += yoffset; if(pitch > 89.0f) pitch = 89.0f; if(pitch < -89.0f) pitch = -89.0f; glm::vec3 front; front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch)); front.y = sin(glm::radians(pitch)); front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch)); cameraFront = glm::normalize(front); } // 鼠标滚轮变化 void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { if(fov >= 1.0f && fov <= 45.0f) fov -= yoffset; if(fov <= 1.0f) fov = 1.0f; if(fov >= 45.0f) fov = 45.0f; } unsigned int *renderInit() { //配置项 glEnable(GL_DEPTH_TEST); unsigned int VAO; glGenVertexArrays(1, &VAO); glBindVertexArray(VAO); unsigned int texture1; glGenTextures(1, &texture1); glBindTexture(GL_TEXTURE_2D, texture1); // 为当前绑定的纹理对象设置环绕、过滤方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 加载并生成纹理 int width, height, nrChannels; unsigned char *data = stbi_load("../container.jpg", &width, &height, &nrChannels, 0); if (data) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data); unsigned int texture2; glGenTextures(1, &texture2); glBindTexture(GL_TEXTURE_2D, texture2); // 为当前绑定的纹理对象设置环绕、过滤方式 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // // 加载并生成纹理 int width2, height2, nrChannels2; stbi_set_flip_vertically_on_load(true); unsigned char *data2 = stbi_load("../awesomeface.png", &width2, &height2, &nrChannels2, 0); if (data2) { glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width2, height2, 0, GL_RGBA, GL_UNSIGNED_BYTE, data2); glGenerateMipmap(GL_TEXTURE_2D); } else { std::cout << "Failed to load texture" << std::endl; } stbi_image_free(data2); float vertices[] = { -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, 0.5f, -0.5f, -0.5f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 1.0f, 1.0f, -0.5f, 0.5f, 0.5f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -0.5f, 0.5f, -0.5f, 1.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, 0.5f, -0.5f, -0.5f, 1.0f, 1.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, 0.5f, -0.5f, 0.5f, 1.0f, 0.0f, -0.5f, -0.5f, 0.5f, 0.0f, 0.0f, -0.5f, -0.5f, -0.5f, 0.0f, 1.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f, 0.5f, 0.5f, -0.5f, 1.0f, 1.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.5f, 1.0f, 0.0f, -0.5f, 0.5f, 0.5f, 0.0f, 0.0f, -0.5f, 0.5f, -0.5f, 0.0f, 1.0f}; unsigned int VBO; glGenBuffers(1, &VBO); glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void *)(3 * sizeof(float))); glEnableVertexAttribArray(1); Shader shaderProgram = Shader("../test.vs.glsl", "../test.fs.glsl"); shaderProgram.use(); glUniform1i(glGetUniformLocation(shaderProgram.ID, "texture1"), 0); glUniform1i(glGetUniformLocation(shaderProgram.ID, "texture2"), 1); return new unsigned int[5]{shaderProgram.ID, VAO, VBO, texture1, texture2}; } void render(unsigned int shaderProgram, unsigned int VAO, unsigned int texture1, unsigned int texture2) { glClearColor(0.2, 0.3, 0.3, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); glUseProgram(shaderProgram); glm::vec3 cubePositions[] = { glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(2.0f, 5.0f, -15.0f), glm::vec3(-1.5f, -2.2f, -2.5f), glm::vec3(-3.8f, -2.0f, -12.3f), glm::vec3(2.4f, -0.4f, -3.5f), glm::vec3(-1.7f, 3.0f, -7.5f), glm::vec3(1.3f, -2.0f, -2.5f), glm::vec3(1.5f, 2.0f, -2.5f), glm::vec3(1.5f, 0.2f, -1.5f), glm::vec3(-1.3f, 1.0f, -1.5f)}; glm::mat4 view = glm::mat4(1.0f); // 注意,我们将矩阵向我们要进行移动场景的反方向移动。 // view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f)); // float radius = 10.0f; // float camX = sin(glfwGetTime()) * radius; // float camZ = cos(glfwGetTime()) * radius; // view = glm::lookAt(glm::vec3(camX, 20.0f, camZ), // glm::vec3(0.0f, 0.0f, 0.0f), // glm::vec3(0.0f, 1.0f, 0.0f)); view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp); glm::mat4 projection = glm::mat4(1.0f); projection = glm::perspective(glm::radians(fov), 800.0f / 600.0f, 0.1f, 100.0f); // 模型矩阵 int modelLoc = glGetUniformLocation(shaderProgram, "model"); // 观察矩阵 int viewLoc = glGetUniformLocation(shaderProgram, "view"); glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view)); // 投影矩阵 int projectionLoc = glGetUniformLocation(shaderProgram, "projection"); glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection)); glBindVertexArray(VAO); for (unsigned int i = 0; i < 10; i++) { glm::mat4 model = glm::mat4(1.0f); model = glm::translate(model, cubePositions[i]); float angle = 20.0f * (i + 1); model = glm::rotate(model, (float)glfwGetTime() * glm::radians(50.0f), glm::vec3(1.0f, 0.3f, 0.5f)); glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model)); glDrawArrays(GL_TRIANGLES, 0, 36); } }
Shader.hpp
见:基于C++的OpenGL 02 之着色器 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)
顶点着色器test.vs.glsl
:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoord; out vec2 TexCoord; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { // 注意乘法要从右向左读 gl_Position = projection * view * model * vec4(aPos, 1.0); TexCoord = aTexCoord; }
片段着色器test.fs.glsl
:
#version 330 core out vec4 FragColor; in vec2 TexCoord; uniform sampler2D texture1; uniform sampler2D texture2; void main() { FragColor = mix(texture(texture1, TexCoord), texture(texture2, TexCoord), 0.2); }
8. 封装摄像机类
camera.hpp
文件:
#ifndef CAMERA_HPP #define CAMERA_HPP #include <glad/glad.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <vector> // Defines several possible options for camera movement. Used as abstraction to stay away from window-system specific input methods enum Camera_Movement { FORWARD, BACKWARD, LEFT, RIGHT }; // Default camera values const float YAW = -90.0f; const float PITCH = 0.0f; const float SPEED = 2.5f; const float SENSITIVITY = 0.1f; const float ZOOM = 45.0f; // An abstract camera class that processes input and calculates the corresponding Euler Angles, Vectors and Matrices for use in OpenGL class Camera { public: // camera Attributes glm::vec3 Position; glm::vec3 Front; glm::vec3 Up; glm::vec3 Right; glm::vec3 WorldUp; // euler Angles float Yaw; float Pitch; // camera options float MovementSpeed; float MouseSensitivity; float Zoom; // constructor with vectors Camera(glm::vec3 position = glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3 up = glm::vec3(0.0f, 1.0f, 0.0f), float yaw = YAW, float pitch = PITCH) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM) { Position = position; WorldUp = up; Yaw = yaw; Pitch = pitch; updateCameraVectors(); } // constructor with scalar values Camera(float posX, float posY, float posZ, float upX, float upY, float upZ, float yaw, float pitch) : Front(glm::vec3(0.0f, 0.0f, -1.0f)), MovementSpeed(SPEED), MouseSensitivity(SENSITIVITY), Zoom(ZOOM) { Position = glm::vec3(posX, posY, posZ); WorldUp = glm::vec3(upX, upY, upZ); Yaw = yaw; Pitch = pitch; updateCameraVectors(); } // returns the view matrix calculated using Euler Angles and the LookAt Matrix glm::mat4 GetViewMatrix() { return glm::lookAt(Position, Position + Front, Up); } // processes input received from any keyboard-like input system. Accepts input parameter in the form of camera defined ENUM (to abstract it from windowing systems) void ProcessKeyboard(Camera_Movement direction, float deltaTime) { float velocity = MovementSpeed * deltaTime; if (direction == FORWARD) Position += Front * velocity; if (direction == BACKWARD) Position -= Front * velocity; if (direction == LEFT) Position -= Right * velocity; if (direction == RIGHT) Position += Right * velocity; } // processes input received from a mouse input system. Expects the offset value in both the x and y direction. void ProcessMouseMovement(float xoffset, float yoffset, GLboolean constrainPitch = true) { xoffset *= MouseSensitivity; yoffset *= MouseSensitivity; Yaw += xoffset; Pitch += yoffset; // make sure that when pitch is out of bounds, screen doesn't get flipped if (constrainPitch) { if (Pitch > 89.0f) Pitch = 89.0f; if (Pitch < -89.0f) Pitch = -89.0f; } // update Front, Right and Up Vectors using the updated Euler angles updateCameraVectors(); } // processes input received from a mouse scroll-wheel event. Only requires input on the vertical wheel-axis void ProcessMouseScroll(float yoffset) { Zoom -= (float)yoffset; if (Zoom < 1.0f) Zoom = 1.0f; if (Zoom > 45.0f) Zoom = 45.0f; } private: // calculates the front vector from the Camera's (updated) Euler Angles void updateCameraVectors() { // calculate the new Front vector glm::vec3 front; front.x = cos(glm::radians(Yaw)) * cos(glm::radians(Pitch)); front.y = sin(glm::radians(Pitch)); front.z = sin(glm::radians(Yaw)) * cos(glm::radians(Pitch)); Front = glm::normalize(front); // also re-calculate the Right and Up vector Right = glm::normalize(glm::cross(Front, WorldUp)); // normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement. Up = glm::normalize(glm::cross(Right, Front)); } }; #endif
9. 参考资料
[1]摄像机 - LearnOpenGL CN (learnopengl-cn.github.io)
[2]OpenGL学习笔记(八)摄像机 - 知乎 (zhihu.com)
这篇关于基于C++的OpenGL 06 之摄像机的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享