openGL GLFW 学习笔记 m7w4
今天来点大家想看的东西:glsl
参考LearnOpenGL:主页 - LearnOpenGL CN,都是一些零零散散的记录,主要章节参考章节Camera。
着色器包装成类,方便调用,具体代码都在教程里;
纹理(Texture)
- 4种纹理环绕方式,且各坐标轴的环绕方式可以单独设置;
- 纹理过滤(最邻近,线性等);
- Mipmap,openGL可以自动生成纹理的Mipmap,基本作用就是节省计算资源(预计算),Mipmap的过滤方式即考虑生成Mipmap图像时的过滤方式,也考虑在Mip上采样时的过滤方式。
纹理加载/创建
有一直用其他语言并且很多年没有怎么用过C++的友友可能和我有一样的疑问,为什么这里创建一个能够在着色器中使用的纹理这么麻烦呢(包括之前的创建VAO,VBO,EBO)?为啥不能创建一个类似GL_TEXTURE_2D的对象直接使用
(胡言乱语)?unsigned int texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture);
以下是个人理解,texture是一个unsigned int型的变量,glGenTextures会给texture生成一个独特的id(类似于pid之类的东西),glBindTexture把GL_TEXTURE_2D绑定到这个名字上,因为纹理不止有一个,用于支持多纹理,方便管理(这样VAO,VBO,EBO也是同理)。至于后一个问题,我也不知道为啥。
添加背景
总觉得IDE太单调了,用ClaudiaIDE改了下背景,好看多了^ ^
实现效果(RGB渐变铸币大头):
注意:如果用JPG图像因为是三通道所以glTexImage2D参数是GL_RGB,如果是PNG图像则需要是GL_RGBA。
纹理单元索引
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
这块给sampler2D添加的是Int类型的值,实际上是在告诉OpenGL,该uniform变量应该使用纹理单元0上的纹理数据。由于默认使用的就是纹理单元0,所以在仅使用一个纹理的时候不需要设置活动纹理,也不需要给着色器中唯一的sampler2D添加值。
坐标系统
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
了解这坐标空间都是干嘛的,更进一步的可以去Games101学习^ ^强推 ;
如果之前有看过的但不记得的可以看这篇,写的很清楚:Unity3D - Shader - 模型、世界、观察、裁剪空间坐标转换_unity shader 模型空间到世界空间计算过程-CSDN博客
GLM库需要自行安装,可以用ImGUI查看和改变各种变量;
这里附上一个ImGUI的基础使用指南(最最最基础的!)
参考ImGUI的glfw opengl3示例,ImGUI是渲染到glfw窗口内的。
//ImGUI
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
const char* glsl_version = "#version 330 core";
ImGui_ImplOpenGL3_Init(glsl_version);
while (!glfwWindowShouldClose(window))
{
// 渲染相关处理...
// imGUI显示
// Start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it.
ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too)
ImGui::End();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
glfwPollEvents()相关:
回调函数注册: 在初始化 GLFW 窗口时,通过
glfwSetKeyCallback()
等函数将自定义的回调函数注册到 GLFW 窗口对象上。键盘输入处理: 当用户按下或释放键盘按键时,GLFW 会检测到这些事件。在调用
glfwPollEvents()
时,glfwPollEvents()
函数会处理当前窗口的所有未处理事件,并触发注册的回调函数(比如key_callback
函数)。
设置相机
万向锁这一块:几何学——欧拉角与万向锁看这一篇就够了(含threejs demo演示) - ICE - 图形学社区
简单来说:在动态变化的坐标系下(物体/局部坐标系),由于旋转的顺序执行,导致的这种角度为±90°的第二次旋转使得第一次和第三次旋转的旋转轴相同的现象,称作万向锁。
计算圆上坐标这一块:
float X = sin(value) * radius; float Y = cos(value) * radius;
只需要一个单调均匀变化的浮点数,可以是value也可以是其他的;
关于glfw回调函数void mouse_callback(GLFWwindow* window, double xpos, double ypos)这一块。鼠标第一次移动时会触发回调函数,这时给到的位置值和鼠标在屏幕上的位置有关。然后移动鼠标,位置值会跟随鼠标移动变化(不考虑数据类型范围,应该是无边界值的,也就是位置值不会现在在分辨率范围内)。此时如果不操作该窗口(把当前操作窗口切换到别的窗口),鼠标会出现在上一次消失的位置,此时随意移动鼠标,不会触发回调函数。再次进入该窗口后的第一次移动鼠标,位置值会继续从上次消失时的位置开始计算,而不是和创建窗口后第一次移动鼠标一样使用屏幕上的位置;
欧拉角计算方向
通过欧拉角(yaw和pitch)可以算出来一个方向向量cameraFront,表明相机的朝向;
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);
然后用lookAt函数,传入相机位置,目标点位置(这里用相机位置 + 相机朝向计算),向上方向(注!这里的向上方向是前面定义的一个固定值 glm::vec3 cameraUp = glm::vec3(0.0f, 1.0f, 0.0f););
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);
这个代码能正常运行,这说明cameraFront和cameraUp不需要相互垂直,但是前面不是说cameraFront和cameraUp需要互相垂直嘛,怎么会事呢?o.O
咱进到lookAt函数中看一下就知道了,这里eye就是cameraPos,center是cameraPos + cameraFront,up是cameraUp。虽然我们给了一个cameraUp,但在lookAt函数中会用center和eye先计算观察方向向量(也就是cameraFront,绕来绕去又绕回来了),再计算向右方向,用up和f(等同于cameraFront),再重新计算向上方向。
vec<3, T, Q> const f(normalize(center - eye));
vec<3, T, Q> const s(normalize(cross(f, up)));
vec<3, T, Q> const u(cross(s, f));
所以,实际上代码中的cameraUp只是确保了我们的相机的向上方向与观察方向构成的平面在竖直方向上,而不是我们理解上的向上方向;
这样的话,如果f和up具有相同的方向,那s和u都会是零向量,这样就出问题了(实际上代码中限制了pitch,所以f不会和up同向)。