OpenGL实现billboard效果(CPU)
实现的原理
无论怎么旋转视角,它都面向摄像机;但大小会随着远近而变化。
使物体的右方向始终与摄像机的有方向平行。布告板的顶点坐标根据相机的右方向自动计算,并且布告板始终面向摄像机。如下图所示:
关于布告板的计算
由用户确定布告板的大小和下端中点坐标:
struct Billboard { glm::vec2 _size; // 布告板大小 glm::vec3 _pos; // 布告板最下端中点的位置 };
根据摄像机的右方向计算布告板的四个顶点:
glm::vec3 faceDir = camera.Right; /// 计算布告板的四个点,根据摄像机camera._right,可以理解为水平(x轴) glm::vec3 lb = billboard._pos - faceDir * billboard._size.x * 0.5f; glm::vec3 rb = billboard._pos + faceDir * billboard._size.x * 0.5f; /// 计算四个点的下面两个点,上面的点就是下面的,在垂直方向增加一个高度 glm::vec3 lt = lb + glm::vec3(0, billboard._size.y, 0); glm::vec3 rt = rb + glm::vec3(0, billboard._size.y, 0); /// 绘制一个草需要两个三角形,六个点 struct ObjectVertex { glm::vec3 _pos; glm::vec2 _uv; }; ObjectVertex transparentVertices[6] = { { lb, glm::vec2(0,1)}, { rb, glm::vec2(1,1)}, { rt, glm::vec2(1,0)}, { lb, glm::vec2(0,1)}, { rt, glm::vec2(1,0)}, { lt, glm::vec2(0,0)}, };
顶点着色器 billboard.vs
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoords; out vec2 TexCoords; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { TexCoords = aTexCoords; gl_Position = projection * view * model * vec4(aPos, 1.0); }
片段着色器 billboard.fs
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D texture1; void main() { vec4 texColor = texture(texture1, TexCoords); if(texColor.a < 0.1) discard; FragColor = texColor; }
完整代码实现
//#include <glew/glew.h> #include <glad/glad.h> #include <GLFW/glfw3.h> #include <stb_image.h> #include <glm/glm.hpp> #include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/type_ptr.hpp> #include <learnopengl/shader_m.h> #include <learnopengl/camera.h> #include <learnopengl/model.h> #include <iostream> void framebuffer_size_callback(GLFWwindow* window, int width, int height); void mouse_callback(GLFWwindow* window, double xpos, double ypos); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); void processInput(GLFWwindow *window); unsigned int loadTexture(const char *path); // settings const unsigned int SCR_WIDTH = 800; const unsigned int SCR_HEIGHT = 600; // camera Camera camera(glm::vec3(0.0f, 1.0f, 10.0f)); float lastX = (float)SCR_WIDTH / 2.0; float lastY = (float)SCR_HEIGHT / 2.0; bool firstMouse = true; // timing float deltaTime = 0.0f; float lastFrame = 0.0f; // lighting glm::vec3 lightPos(10.0f, 5.0f, 10.0f); struct ObjectVertex { glm::vec3 _pos; glm::vec2 _uv; }; struct Billboard { glm::vec2 _size; // 布告板大小 glm::vec3 _pos; // 布告板最下端中点的位置 }; int main() { // glfw: initialize and configure // ------------------------------ glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); #ifdef __APPLE__ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); #endif // glfw window creation // -------------------- GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); glfwSetCursorPosCallback(window, mouse_callback); glfwSetScrollCallback(window, scroll_callback); // tell GLFW to capture our mouse //glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); // glad: load all OpenGL function pointers // --------------------------------------- if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // configure global opengl state // ----------------------------- glEnable(GL_DEPTH_TEST); // build and compile shaders // ------------------------- Shader lightingShader("lighting_maps.vs", "lighting_maps.fs"); Shader billboardShader("billboard.vs", "billboard.fs"); // set up vertex data (and buffer(s)) and configure vertex attributes // ------------------------------------------------------------------ float planeVertices[] = { // positions // normals // texture Coords 5.0f, 0.0f, 5.0f, 0.0f, 0.0f, 1.0f, 2.0f, 0.0f, -5.0f, 0.0f, 5.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, -5.0f, 0.0f, -5.0f, 0.0f, 0.0f, 1.0f, 0.0f, 2.0f, 5.0f, 0.0f, 5.0f, 0.0f, 0.0f, 1.0f, 2.0f, 0.0f, -5.0f,0.0f, -5.0f, 0.0f, 0.0f, 1.0f, 0.0f, 2.0f, 5.0f, 0.0f, -5.0f, 0.0f, 0.0f, 1.0f, 2.0f, 2.0f }; // plane VAO unsigned int planeVAO, planeVBO; glGenVertexArrays(1, &planeVAO); glGenBuffers(1, &planeVBO); glBindVertexArray(planeVAO); glBindBuffer(GL_ARRAY_BUFFER, planeVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), &planeVertices, GL_STATIC_DRAW); glBindVertexArray(planeVAO); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float))); glEnableVertexAttribArray(2); // transparent VAO unsigned int transparentVAO, transparentVBO; glGenVertexArrays(1, &transparentVAO); glGenBuffers(1, &transparentVBO); // load textures // ------------- unsigned int floorTexture = loadTexture("resources/textures/metal.png"); unsigned int transparentTexture = loadTexture("resources/textures/grass.png"); // transparent vegetation locations // -------------------------------- vector<glm::vec3> vegetation; float height = 0.0f; for (float x = -2 ; x < 2 ; x += 1) { for (float z = -2 ; z < 2 ; z += 1) { vegetation.push_back(glm::vec3(x,height,z)); } } // shader configuration // -------------------- lightingShader.use(); lightingShader.setInt("material.diffuse", 0); billboardShader.use(); billboardShader.setInt("texture1", 0); // Billboard billboard; billboard._size = glm::vec2(1.0f, 1.0f); billboard._pos = glm::vec3(0.0f, 0.0f, 0.0f); // render loop // ----------- while (!glfwWindowShouldClose(window)) { // per-frame time logic // -------------------- float currentFrame = static_cast<float>(glfwGetTime()); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; // input // ----- processInput(window); // render // ------ glClearColor(0.1f, 0.1f, 0.1f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // draw objects lightingShader.use(); lightingShader.setVec3("light.position", lightPos); lightingShader.setVec3("viewPos", camera.Position); // light properties lightingShader.setVec3("light.ambient", 0.2f, 0.2f, 0.2f); lightingShader.setVec3("light.diffuse", 0.5f, 0.5f, 0.5f); lightingShader.setVec3("light.specular", 1.0f, 1.0f, 1.0f); // material properties lightingShader.setVec3("material.specular", 0.5f, 0.5f, 0.5f); lightingShader.setFloat("material.shininess", 64.0f); // view/projection transformations glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); glm::mat4 view = camera.GetViewMatrix(); lightingShader.setMat4("projection", projection); lightingShader.setMat4("view", view); // world transformation glm::mat4 model = glm::mat4(1.0f); lightingShader.setMat4("model", model); // bind diffuse map glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, floorTexture); // render the floor glBindVertexArray(planeVAO); glDrawArrays(GL_TRIANGLES, 0, 6); glBindVertexArray(0); // vegetation billboardShader.use(); glm::mat4 projection2 = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f); glm::mat4 view2 = camera.GetViewMatrix(); glm::mat4 model2 = glm::mat4(1.0f); billboardShader.setMat4("projection", projection2); billboardShader.setMat4("view", view2); glm::vec3 faceDir = camera.Right; /// 计算布告板的四个点,根据摄像机camera._right,可以理解为水平(x轴) glm::vec3 lb = billboard._pos - faceDir * billboard._size.x * 0.5f; glm::vec3 rb = billboard._pos + faceDir * billboard._size.x * 0.5f; /// 计算四个点的下面两个点,上面的点就是下面的,在垂直方向增加一个高度 glm::vec3 lt = lb + glm::vec3(0, billboard._size.y, 0); glm::vec3 rt = rb + glm::vec3(0, billboard._size.y, 0); /// 绘制一个草需要两个三角形,六个点 ObjectVertex transparentVertices[6] = { { lb, glm::vec2(0,1)}, { rb, glm::vec2(1,1)}, { rt, glm::vec2(1,0)}, { lb, glm::vec2(0,1)}, { rt, glm::vec2(1,0)}, { lt, glm::vec2(0,0)}, }; glBindVertexArray(transparentVAO); glBindBuffer(GL_ARRAY_BUFFER, transparentVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(transparentVertices), transparentVertices, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float))); glBindVertexArray(0); glBindVertexArray(transparentVAO); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, transparentTexture); for (unsigned int i = 0; i < vegetation.size(); i++) { model2 = glm::mat4(1.0f); model2 = glm::translate(model2, vegetation[i]); billboardShader.setMat4("model", model2); glDrawArrays(GL_TRIANGLES, 0, 6); } // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.) // ------------------------------------------------------------------------------- glfwSwapBuffers(window); glfwPollEvents(); } // optional: de-allocate all resources once they've outlived their purpose: // ------------------------------------------------------------------------ glDeleteVertexArrays(1, &planeVAO); glDeleteBuffers(1, &planeVBO); glfwTerminate(); return 0; } // process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly // --------------------------------------------------------------------------------------------------------- void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) camera.ProcessKeyboard(FORWARD, deltaTime); if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) camera.ProcessKeyboard(BACKWARD, deltaTime); if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS) camera.ProcessKeyboard(LEFT, deltaTime); if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS) camera.ProcessKeyboard(RIGHT, deltaTime); } // glfw: whenever the window size changed (by OS or user resize) this callback function executes // --------------------------------------------------------------------------------------------- void framebuffer_size_callback(GLFWwindow* window, int width, int height) { // make sure the viewport matches the new window dimensions; note that width and // height will be significantly larger than specified on retina displays. glViewport(0, 0, width, height); } // glfw: whenever the mouse moves, this callback is called // ------------------------------------------------------- void mouse_callback(GLFWwindow* window, double xposIn, double yposIn) { float xpos = static_cast<float>(xposIn); float ypos = static_cast<float>(yposIn); if (firstMouse) { lastX = xpos; lastY = ypos; firstMouse = false; } float xoffset = xpos - lastX; float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top lastX = xpos; lastY = ypos; camera.ProcessMouseMovement(xoffset, yoffset); } // glfw: whenever the mouse scroll wheel scrolls, this callback is called // ---------------------------------------------------------------------- void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { camera.ProcessMouseScroll(static_cast<float>(yoffset)); } // utility function for loading a 2D texture from file // --------------------------------------------------- unsigned int loadTexture(char const * path) { unsigned int textureID; glGenTextures(1, &textureID); int width, height, nrComponents; unsigned char *data = stbi_load(path, &width, &height, &nrComponents, 0); if (data) { GLenum format; if (nrComponents == 1) format = GL_RED; else if (nrComponents == 3) format = GL_RGB; else if (nrComponents == 4) format = GL_RGBA; glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data); glGenerateMipmap(GL_TEXTURE_2D); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT); // for this tutorial: use GL_CLAMP_TO_EDGE to prevent semi-transparent borders. Due to interpolation it takes texels from next repeat glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, format == GL_RGBA ? GL_CLAMP_TO_EDGE : GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); stbi_image_free(data); } else { std::cout << "Texture failed to load at path: " << path << std::endl; stbi_image_free(data); } return textureID; }
完整项目代码
https://github.com/mc-liyanliang/OpenGL-Shader/tree/master