OpenGL Getting Started!

最近一周开始学OpenGL。因为选了图形学的课,有不少作业呀,比如写个简单的物理引擎、写个简单的地形引擎、实现某篇论文等等。

科班出身的大概都是从OpenGL开始接触的图形学吧,像我这种野路子进阶路线比较诡异。最开始当然接触的是Rhino,不过Rhino里面都是固定渲染管线,只是熟悉了一些Nurbs和Mesh的几何特性。后来写了些Processing,相当于一个简化的JAVA封装版OpenGL。Processing 2.0大约是OpenGL 2.0,3.0就对应3.0。当时学写的时候用的2.0。  再之后过了一遍乐乐学姐的UnityShader入门精要,基本上是入门Shader的同时完整入门了下图形学的知识。在之后就是翻书突击图形学了。像我这种先写过Shaderlab再学OpenGL的应该不多吧。

图形学的课居然教的还是OpenGL 1.1/2.0,用Nehe Tutorial。这TM就很尴尬了。于是找了OpenGL 3.3的一套教程,Learning OpenGL。已经跟着写了一遍前三章的代码。从零开始到写Shader实现Phong光照模型,到实现多光源的前向渲染场景,到载入模型。理解了不少Unity内部的原理,比如drawcall到底长啥样,batch是啥,前向渲染内部原理,三种灯光参数与原理。后面几章的内容我看也不错,有阴影,延迟渲染,SSAO等等。

OpenGL这个语法真是繁琐,让我非常怀念Processing里面简化的操作。(我用的是GLEW GLFW库)

比如初始化一个窗口,要先设定OpenGL版本,然后创建窗口,创建context

// Init GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    GLFWwindow* window = glfwCreateWindow(screenWidth, screenHeight, "LearnOpenGL", nullptr, nullptr); // Windowed
    glfwMakeContextCurrent(window);

Processing里可是一行就搞定了

size(640, 360);

比如鼠标键盘输入,GLFW里先要绑定一堆事件回调函数。之后每个回调函数的参数中会有相关变量,比如下面这个mouse_callback参数里的xpos,ypos就是光标位置

// Set the required callback functions
    glfwSetKeyCallback(window, key_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);
    glfwSetMouseButtonCallback(window, mousebutton_callback);

    void mouse_callback(GLFWwindow\* window, double xpos, double ypos)
    {
    ...
    }

Processing里呢,直接有俩全局变量mouseX, mouseY存的是光标位置。IO的回调函数也是固定名称,不用自己再绑定了。

void mousePressed();
void mouseDragged();
void mouseReleased();

链接shader也是;我没有在processing里写过shader,但在Unity里可是直接有方便的shaderlab写,然后通过材质链接上shader,再赋到模型上。
OpenGL里好变态啊,以一个vertex shader为例。

//1. 首先把GLSL代码从文件里读出来,转成GLchar*
    std::string vertexCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    vShaderFile.open(vertexPath);
    std::stringstream vShaderStream;
    vShaderStream << vShaderFile.rdbuf();
    vShaderFile.close();
    vertexCode = vShaderStream.str();
    const GLchar* vShaderCode = vertexCode.c_str();
//2. 编译shader
    GLuint vertex;
    GLint success;
    GLchar infoLog[512];
    // Vertex Shader
  vertex = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertex, 1, &vShaderCode, NULL);
  glCompileShader(vertex);
  checkCompileErrors(vertex, "VERTEX");
//3. 绑定shader到program
    GLuint Program;
    Program = glCreateProgram();
    glAttachShader(Program, vertex);
  glLinkProgram(Program);
  checkCompileErrors(Program, "PROGRAM");
  glDeleteShader(vertex);
//4. 使用shader
    glUseProgram(Program);

恶心死了。好在LearningOpengl这套程序里封装了一个shader类
shader里面的public参数如果再unity里,直接在控制板上改数值就好了。在opengl里先要shader里把参数设成uniform,然后程序好长一句话绑定参数

//比如如果我们shader里有一个uniform vec3 ViewPos的变量,就要像下面这样设定
glUniform3f(glGetUniformLocation(Program, "ViewPos"), camera.Position.x, camera.Position.y, camera.Position.z);

这时候如果我们想设置一个贴图,那就更麻烦了。无比怀念unity里拖一张贴图进入材质的操作。

//1. 载入纹理,这里用了SOIL库。先生成一个纹理,绑定到当前环境,设置参数,设置贴图,生成mipmap,解绑纹理。
    int wid_tex, hei_tex;
    unsigned char* image = SOIL_load_image("container2.png", &wid_tex, &hei_tex, 0, SOIL_LOAD_RGB);
    GLuint texture_D;
    glGenTextures(1, &texture_D);
    glBindTexture(GL_TEXTURE_2D, texture_D);

    //设置wrapping
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    // 设置filtering
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, wid_tex, hei_tex, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
    glGenerateMipmap(GL_TEXTURE_2D);
    SOIL_free_image_data(image);
    glBindTexture(GL_TEXTURE_2D, 0);
//2. 给shader赋纹理,先得active纹理绑定到当前context,然后再找shader属性附纹理
    glActiveTexture(texture_D);
    glBindTexture(GL_TEXTURE_2D, texture_D);
    glUniform1i(glGetUniformLocation(myShader.Program, "material.diffuseMap"), 0);

还有像shaderlab里有好多内置的参数,像UNITY_MATRIX_MVP这种,直接获取当前相机的一些变换矩阵。
OpenGL里都得自己生成这些矩阵,然后自己赋给shader。。。。。

还有更恶心的是那个VAO/VBO/EBO了,OpenGL3.3取消了glBegin(), glEnd()这种画图形的方式,所有图形都是用VBO来存储的
我的理解是,VBO是一个buffer,存储了网格模型的顶点信息,包括顶点位置、法线、贴图坐标、颜色等等很多;VAO记录了某个网格模型的VBO数据怎么读取,比如第几个位置的数据是顶点,是法线。。。
EBO存储了面片信息,也就是几号几号顶点组成了一个面片。
一般初始化的时候

    GLuint VBO, VAO, EBO;

//初始化VAO VBO EBO
    glGenBuffers(1, &VBO);
    glGenVertexArrays(1, &rVAO);
    glGenBuffers(1, &EBO);

//VAO绑定到context
    glBindVertexArray(VAO);

    //VBO绑定到context
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //VBO放入顶点数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

//EBO绑定,放入数据
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //设置VAO指针,用来访问VBO中的数据
    //第一个,大小为3个GLfloat,偏移0,对应shader里layout (position = 0) vec3 xxx
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
        //第二个,大小为3个GLfloat,偏移3个GLfloat,对应shader里layout (position = 1) vec3 xxx
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

//解除绑定VAO
    glBindVertexArray(0);

初始化以后,在loop中绘制

//绑定VAO,绘制,解绑VAO
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);

或者用EBO绘制

//绑定VAO,绘制,解绑VAO
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

虽然极为繁琐,但是非常有助于理解GPU渲染管线。这里一个VBO大概就是unity里一个drawcall吧。unity会把同样材质的模型组合一个drawcall渲出来,叫做batching。放在opengl里就是模型放到一个VAO VBO EBO里面。Drawcall是cpu调用的,所以太多可能影响性能。

还有一个学期的OpenGL要写,慢慢来咯。

发表评论

电子邮件地址不会被公开。 必填项已用*标注