OpenGL学习笔记——坐标转换

因为OpenGL中的坐标转换有些复杂,所以做一篇笔记记录一下。

一、简介

学习OpenGL一段时间之后,数据的坐标转换将会成为一个令人头疼的问题,因为我们总不能一直只使用窗口的规范化设备坐标系(NDC)来显示数据,这样会对我们产生很大的约束,而如果我们要把我们真实世界中的东西在OpenGL中显示出来,就必须学会使用坐标转换。

在真正进行坐标转换之前,我们首先要了解OpenGL中到底有什么坐标空间,总的来说,其共有五种坐标空间:局部空间(或者称作模型空间)、世界空间、观察空间(或者称作人眼空间)、裁剪空间以及屏幕空间。
1、我们所有的顶点数据刚一开始都位于局部空间之中,这个空间有点类似于我们生活的真实空间,空间内的模型可能很大,几米甚至几十米等。那么这么大的东西如果直接一比一的放入电脑之中,那电脑屏幕什么也不会显示出来。另一方面则是每一个对象都会拥有一套属于自己的局部坐标系,局部空间内对象的原点可能是(0,0,0),也有可能不是,这不利于我们对这些对象进行定位;再者就是局部空间可能很小(根据Learning OpenGL中的内容进行的一些猜测)。总的来说,局部空间就像是专门为了承载单个对象模型而创建的小天地,而每个对象模型的小天地都是独立存在的,没有任何关系,所以我们需要将他们放到一个统一的坐标系(世界坐标系)下,这样我们才能更好的去描述他们之间的相对关系,所以在这个过程中我们可能就需要对我们的模型进行平移、旋转和缩放等操作。
2、这个世界空间为了方便理解,我们可以将其想象成一种游戏空间,玩过游戏的人都清楚游戏内的对战场景会把多个人物模型给放在一起,也就是放到同一个空间之中进行对战,而这个对战空间其实就有点类似于我们所说的世界空间,它是一个更为庞大的空间。
3、之后我们就是要模拟人类看东西的过程,将物体置入观察空间,也就是让我们可以看到这个物体。这个过程有点类似于将一个照相机移到了模型前方的某个位置,然后再设置一下照相机的朝向,让这个照相机可以看到我们所置入的模型。
4、在观察空间中,类比于现实中的人类,我们视域总是有限的,也就是我们能看到的东西总是有限的,所以在OpenGL中其也会让我们去设置一个视域范围,如果模型存在一部分超出了我们设置的视域范围,屏幕将不会显示该部分的模型。
5、当前面都设置好了之后只需要在设置一下视口(一般与窗口大小相同),就可以将模型在屏幕上显示出来。

详细的内容可以参看:https://learnopengl.com/Getting-started/Coordinate-Systems

二、代码实现

2.1简单的测试

Shader.h

#ifndef SHADER_H
#define SHADER_H

#include<glad\glad.h>

#include<string>
#include<fstream>
#include<sstream>       //字符流
#include<iostream>

class Shader
{
public:
    //着色器程序的ID
    unsigned int ID;

    //构造函数
    Shader(const char* vertexPath, const char* fragmentPath);

    //激活着色器
    void use();

    //设置着色器中的转换模型
    void setMat4(const std::string& name ,const glm::mat4 &mat) const;

};

Shader::Shader(const char* vertexPath, const char* fragmentPath) {
    //检索顶点和片元着色器源代码的路径
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;

    //如果发生以下错误程序将会抛出异常,使用下面的语句可以保证异常的抛出
    vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);     //其实就是重置了输入输出和文件读取写入的状态标记
    fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);

    try
    {
        //打开文件
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        //文件的缓冲区内容读入sstream对象中
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();

        //关闭文件
        vShaderFile.close();
        fShaderFile.close();

        //将字符流转换为string字符串
        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch (const std::exception&)
    {
        std::cout << "文件读取失败!" << std::endl;
    }

    const char* vShaderCode = vertexCode.c_str();       //将字符串中的字符数组赋给一个字符指针
    const char* fShaderCode = fragmentCode.c_str();
    
    //
    //编译着色器
    //
    unsigned int vertex, fragment;      //记录着色器对象的索引
    int success;        //记录着色器编译成功与否
    char infolog[512];

    //顶点着色器
    vertex = glCreateShader(GL_VERTEX_SHADER);      //创建一个空的着色器对象,把源代码填进去就可以制作顶点着色器程序了
    glShaderSource(vertex, 1, &vShaderCode, NULL);      //将Shader中的源代码设置为指定数组中的字符串,之前Shader对象中的原代码将会被替代
    glCompileShader(vertex);    //编译顶点着色器

    //检查着色器编译的情况,如果有错误打印出来
    glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertex, 512, NULL, infolog);
        std::cout << "顶点着色器编译错误信息:" << infolog << std::endl;
    }

    //片元着色器
    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &fShaderCode, NULL);
    glCompileShader(fragment);
    glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragment, 512, NULL, infolog);
        std::cout << "片元着色器编译错误信息:" << infolog << std::endl;
    }

    //编译为着色器程序,将着色器对象添加到一个程序对象上去,其实也就是链接两个着色器对象
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    glGetProgramiv(ID, GL_LINK_STATUS, &success);
    if (!success)
    {
        glGetProgramInfoLog(ID, 512, NULL, infolog);
        std::cout << "着色器链接错误信息:" << infolog << std::endl;
    }

    //将创建的着色器对象给删除掉
    glDeleteShader(vertex);
    glDeleteShader(fragment);
}

void Shader::use() {
    glUseProgram(ID);       //激活着色器程序
}

void Shader::setMat4(const std::string& name, const glm::mat4& mat)const {
    glUniformMatrix4fv(glGetUniformLocation(this->ID, name.c_str()), 1, GL_FALSE, &mat[0][0]);      //设置指定转换的模型的值
}


#endif // !SHADER_H

CT.h

#ifndef CT_H
#define CT_H

//一般的C++头文件
#include<iostream>
#include<vector>

//GLAD
#include<glad\glad.h>           //用于初始化我们所需的OpenGL函数

//GLFW
#include<GLFW\glfw3.h>      //用于处理窗口相关的事件

//glm
#include<glm\glm.hpp>
#include<glm\gtc\matrix_transform.hpp>
#include<glm\gtc\type_ptr.hpp>

//自定义的类头文件
#include"Shader.h"

//相关的函数原型
//void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

namespace CT {
    //窗口相关信息
    const GLuint WIDTH = 800, HEIGHT = 600;
    GLfloat lastX = 400, lastY = 300;

    void CT_test() {
        //初始化GLFW函数库,操作系统会分配相应的资源
        glfwInit();

        //设置我们所需要的一些选项,也就是配置GLFW
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);      //GLFW所使用的OpenGL版本为3.3版本,如果用户没有3.3版本,则GLFW不能运行
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);      //使用OpenGL的核心模式
        glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

        //使用GLFW函数创建一个窗口
        GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "CoordinateTransform", nullptr, nullptr);
        if (window == NULL)
        {
            std::cout << "创建窗口失败!" << std::endl;
            glfwTerminate();    //终止进程并释放资源
            return;
        }

        glfwMakeContextCurrent(window);         //创建当前上下文环境

                                                //初始化GLAD,在我们调用OpenGL函数之前,然后我们就可以直接使用函数了,这类似于将显式链接变为了隐式链接,但是其查找过程并没有改变
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {

            std::cout << "初始化GLAD失败\n";
            return;

        }

        //设置回调函数让Windows进行调用
        //glfwSetKeyCallback(window, key_callback);

        //设置视口,也就是我们可见的区域
        glViewport(0, 0, WIDTH, HEIGHT);

        //创建与编译着色器程序
        Shader myShader("../Shader/shader_ct.vs", "../Shader/shader_ct.fs");

        //定义顶点数据
        //std::vector<GLfloat> vertice;     //OpenGL中的函数会接收数组型的数据
        GLfloat vertice[] = {
            0.0,100,0.0,
            -100,0.0,0.0,
            100,0.0,0.0
        };

        //
        //显示部分
        //
        GLuint VBO, VAO;
        glGenVertexArrays(1, &VAO);     //创建顶点数组对象的名字,其更像是一组指针,负责管理缓冲对象
        glGenBuffers(1, &VBO);          //创建缓冲区对象名字,分配缓存,负责保存一系列顶点的数据
        glBindVertexArray(VAO);     //绑定一个顶点数组对象,仅仅只有对象还是没有用处的,我们还需要将他们与当前的OpenGL环境进行绑定才能够被OpenGL使用,也就是激活它
        glBindBuffer(GL_ARRAY_BUFFER, VBO);     //绑定一个命名的缓冲区对象
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertice), &vertice[0], GL_STATIC_DRAW);        //创建和初始化一个缓冲区对象的数据存储

                                                                                            //位置属性
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GL_FLOAT), (GLvoid*)0);      //指定顶点位置数据
        glEnableVertexAttribArray(0);       //启用了布局标志符为0的变量

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0); // Unbind VAO
        glEnable(GL_DEPTH_TEST);        //启用深度测试
        glPointSize(10);

        while (!glfwWindowShouldClose(window))
        {
            glfwPollEvents();       //监视所有的窗口事件
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f);       //用什么样的颜色来清除之前的缓冲区
                                                        //glClear(GL_COLOR_BUFFER_BIT);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            myShader.use();
            glBindVertexArray(VAO);

            //
            //坐标转换
            //
            //模型变换,将模型置入世界空间之中
            glm::mat4 model(1);
            model = glm::translate(model, glm::vec3(0, 0, 0));      //将模型平移到点(0,0,0)处
            myShader.setMat4("model", model);
            //视图变换
            glm::mat4 view;
            view = glm::lookAt(glm::vec3(0.0f, 0.0f, 250.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
            myShader.setMat4("view", view);
            //投影变换
            glm::mat4 prj;
            prj = glm::perspective(glm::radians(45.0f), (GLfloat)WIDTH / (GLfloat)HEIGHT, 0.1f, 500.0f);
            myShader.setMat4("prj", prj);


            //glDrawArrays(GL_POINTS, 0, sizeof(vertice)/(3*sizeof(vertice[0])));       //绘制点
            glDrawArrays(GL_TRIANGLES, 0, sizeof(vertice) / (3 * sizeof(vertice[0])));
            glfwSwapBuffers(window);        //交换缓冲区

        }

        //释放掉相应的资源
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(1, &VBO);

        //清理窗口
        glfwDestroyWindow(window);
        glfwTerminate();        //清除掉分配给glfw的资源
    
    }
}

#endif // !CT_H

main.cpp

#include"CT.h"

int main() {

    CT::CT_test();

    return 0;
}

着色器代码:

shader_ct.vs

#version 330 core

//进行布局
layout (location=0) in vec3 aPos;       //接收传进来的数据
out vec4 myColor;

//定义坐标转换变量
uniform mat4 model;
uniform mat4 view;
uniform mat4 prj;

void main(){
    gl_Position = prj * view * model * vec4(aPos, 1.0);     //对顶点数据进行坐标转换
    myColor = vec4(0,1,0,1);
}

shader_ct.fs

#version 330 core

in vec4 myColor;        //接收传过来的数据
out vec4 fragColor;

void main(){
    fragColor = myColor;
}

测试效果:


2.2旋转测试

在model模型变换矩阵的代码中添加下面的代码:

//模型变换,将模型置入世界空间之中
glm::mat4 model(1);
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::translate(model, glm::vec3(0, 0, 0));      //将模型平移到点(0,0,0)处
myShader.setMat4("model", model);

测试效果:


三、小结

为什么要搞这么多坐标空间呢?这是我在学习这部分的时候最大的疑问,《learning OpenGL》一书中对这个问题倒是有很好的解答。每一个空间其实都是为了后续的一些特定的操作而服务的,如对象自身的一些修改,那自然是在局部空间中进行这些修改操作是较好的;而如果要使用对象与对象之间的相对关系,则是在世界空间中更为合适,这类的操作可能还有很多。OpenGL的设计者肯定也是考虑过这些问题的,前面的繁琐是为了后面的便捷,而我现阶段要做的就是学会怎么灵活的使用它就很不错了~

参考资料:《Learning OpenGL》《OpenGL编程指南》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容