精华内容
下载资源
问答
  • Opengl 坐标

    千次阅读 2016-12-30 13:57:18
    继续 Learn Opengl 在流水线(Pipe line)中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统。下图展示了整个流程: 局部空间 局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在...

              继续 Learn Opengl


    在流水线(Pipe line)中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统。下图展示了整个流程:

    coordinate_systems

    局部空间

    局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在一个建模软件中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),即便它有可能最后在程序中处于完全不同的位置。你的模型的所有顶点都是在局部空间中:它们相对于你的物体来说都是局部的。局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。


    世界空间

    世界空间中的坐标是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。

    模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。

    观察空间

    观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。

    裁剪空间

    在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。

    因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它变换回标准化设备坐标系,就像OpenGL期望的那样。

    为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250, 500, 750)将是不可见的,这是由于它的x坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,所以被裁剪掉了。

    如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。

    由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。

    一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。

    在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。

    将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

    正交投影

    正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵变换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:

    orthographic projection frustum

    上面的平截头体定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。

    透视投影

    透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:

    perspective

    正如你看到的那样,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用透视投影矩阵来完成的。这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:


    顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。

    cocos2dx中 这样创建一个透视投影矩阵:
        projection = new Mat4();
        Mat4::createPerspective(45, viewsize.width/viewsize.height, 0.1, 200, projection);

    下面是一张透视平截头体的图片:

    perspective_frustum

    它的第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。

    当你把透视矩阵的 near 值设置太大时(如10.0f),OpenGL会将靠近摄像机的坐标(在0.0f和10.0f之间)都裁剪掉,这会导致一个你在游戏中很熟悉的视觉效果:在太过靠近一个物体的时候你的视线会直接穿过去。

    当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。某些如 Blender 等进行三维建模的软件有时在建模时也会使用正射投影,因为它在各个维度下都更准确地描绘了每个物体。下面你能够看到在Blender里面使用两种投影方式的对比:

    perspective_orthographic

    你可以看到,使用透视投影的话,远处的顶点看起来比较小,而在正射投影中每个顶点距离观察者的距离都是一样的。

    以上都组合在一起(viewport transform)

    我们为上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:

    Vclip=MprojectionMviewMmodelVlocal

    注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

    然后呢?

    顶点着色器的输出要求所有的顶点都在裁剪空间内,这正是我们刚才使用变换矩阵所做的。OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中是一个800x600的屏幕)。这个过程称为视口变换。


    进入3D

    在开始进行3D绘图时,我们首先创建一个模型矩阵。这个模型矩阵包含了位移、缩放与旋转操作,它们会被应用到所有物体的顶点上,以变换它们到全局的世界空间。让我们变换一下我们的平面,将其绕着x轴旋转,使它看起来像放在地上一样。这个模型矩阵看起来是这样的:
        model = new Mat4();
        model->rotate(Vec3(1, 0, 0), CC_DEGREES_TO_RADIANS(50));
    通过将顶点坐标乘以这个模型矩阵,我们将该顶点坐标变换到世界坐标。我们的平面看起来就是在地板上,代表全局世界里的平面。

    接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动,以使得物体变成可见的(当在世界空间时,我们位于原点(0,0,0))。

    要想在场景里面移动,先想一想下面这个问题:

    • 将摄像机向后移动,和将整个场景向前移动是一样的。

    这正是观察矩阵所做的,我们以相反于摄像机移动的方向移动整个场景。因为我们想要往后移动,并且OpenGL是一个右手坐标系(Right-handed System),所以我们需要沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现。它会给我们一种我们在往后移动的感觉。

    右手坐标系(Right-handed System)

    按照惯例,OpenGL是一个右手坐标系。简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。坐标系画起来如下:

    coordinate_systems_right_handed

    为了理解为什么被称为右手坐标系,按如下的步骤做:

    • 沿着正y轴方向伸出你的右臂,手指着上方。
    • 大拇指指向右方。
    • 食指指向上方。
    • 中指向下弯曲90度。 

    如果你的动作正确,那么你的大拇指指向正x轴方向,食指指向正y轴方向,中指指向正z轴方向。如果你用左臂来做这些动作,你会发现z轴的方向是相反的。这个叫做左手坐标系,它被DirectX广泛地使用。注意在标准化设备坐标系中OpenGL实际上使用的是左手坐标系(投影矩阵交换了左右手)。

    观察矩阵是这样的:
        view = new Mat4();
        view->translate(0, 0, -3);

    最后我们需要做的是定义一个投影矩阵。我们希望在场景中使用透视投影,所以像这样声明一个投影矩阵:
        cocos2d::Size viewsize = Director::getInstance()->getVisibleSize();
        projection = new Mat4();
        Mat4::createPerspective(45, viewsize.width/viewsize.height, 0.1, 200, projection);
    既然我们已经创建了变换矩阵,我们应该将它们传入着色器。首先,让我们在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标:
    //matrix.vsh
    attribute vec3 a_position;
    attribute vec3 a_color;
    attribute vec2 a_texcoord;
    
    varying vec4 v_vertexColor;
    varying vec2 v_textureCoord;
    
    
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        gl_Position = projection * view * model* vec4(a_position, 1);
        v_vertexColor = vec4(a_color, 1);
        v_textureCoord = vec2(a_texcoord.x, 1.0 - a_texcoord.y);
    }
    

    我们还应该将矩阵传入着色器(这通常在每次的渲染迭代中进行,因为变换矩阵会经常变动):
       //矩阵变换,给vertex shader传值
        GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model->m);
        GLuint viewLoc = glGetUniformLocation(glprogram->getProgram(), "view");
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, view->m);
        GLuint projectionLoc = glGetUniformLocation(glprogram->getProgram(), "projection");
        glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, projection->m);
    

    我们的顶点坐标已经使用模型、观察和投影矩阵进行变换了,最终的物体应该会:

    • 稍微向后倾斜至地板方向。
    • 离我们有一些距离。
    • 有透视效果(顶点越远,变得越小)。
    全部代码:
    //
    //  Triangle.cpp
    //  shaderTest
    //
    //  Created by MacSBL on 2016/12/15.
    //
    //
    
    #include "OpenGLMatrix.h"
    
    
    bool OpenGLMatrix::init()
    {
        if (!Layer::init()) {
            return false;
        }
        
        GLfloat vertices[] = {
            //顶点        //颜色       //纹理坐标
            0.5, 0.5, 0,   1, 0, 0,    1.0, 1.0, //右上
            0.5, -0.5, 0,  0, 1, 0,    1.0, 0, //右下
            -0.5, -0.5, 0, 0, 0, 1,    0, 0, //左下
            -0.5, 0.5, 0,  0, 0, 0,    0, 1,//左上
        };
        GLuint indices[] = {
            0, 1, 3,
            1, 2, 3
        };
        
        //创建并绑定纹理
        //
        //方法1
        auto sprite = Sprite::create("awesomeface.png");
        textureId2 = sprite->getTexture()->getName();
        //
        //方法2
        glGenTextures(1, &textureId);
        glBindTexture(GL_TEXTURE_2D, textureId);
        
        //纹理环绕方式
        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_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        
        //加载纹理
        Image* img = new Image();
        img->initWithImageFile("wall.jpg");
        int width = img->getWidth();
        int height = img->getHeight();
        unsigned char* imgdata = img->getData();
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
        glGenerateMipmap(GL_TEXTURE_2D);
        //释放内存,解绑texture
        CC_SAFE_DELETE(img);
        glBindTexture(GL_TEXTURE_2D, 0);
        ///
        
        //1、绑定vao
        //顶点数组对象(Vertex Array Object)被绑定后,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        
        // 2. 把顶点数组复制到缓冲(vbo)中供OpenGL使用
        //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
        GLuint vbo;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        //把之前定义的顶点数据复制到缓冲的内存中
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        
        //3, ebo
        GLuint ebo;
        glGenBuffers(1, &ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
        
        //解析顶点数据, 应用到第1个顶点属性上
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8* sizeof(GLfloat), (GLvoid*)0);
        glEnableVertexAttribArray(0);
        // 颜色,应用到第2个属性上
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
        glEnableVertexAttribArray(1);
        //纹理坐标,应用到第3个属性上
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
        glEnableVertexAttribArray(2);
        
        //解除绑定vao和vbo
        glBindVertexArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        
        //——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
        auto program = new GLProgram;
        program->initWithFilenames("matrix.vsh", "matrix.fsh");
        program->link();
        
        
        //——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
        //矩阵
        //绕某轴旋转x°
        model = new Mat4();
        model->rotate(Vec3(1, 0, 0), CC_DEGREES_TO_RADIANS(50));
        
        //离我们有一些距离
        view = new Mat4();
        view->translate(0, 0, -3);
        
        //有透视效果(顶点越远,变得越小)
        cocos2d::Size viewsize = Director::getInstance()->getVisibleSize();
        projection = new Mat4();
        Mat4::createPerspective(45, viewsize.width/viewsize.height, 0.1, 200, projection);
        
        
        this->setGLProgram(program);
        
        return true;
    }
    
    void OpenGLMatrix::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
    {
        Layer::visit(renderer, parentTransform, parentFlags);
        _command.init(_globalZOrder);
        _command.func = CC_CALLBACK_0(OpenGLMatrix::onDraw, this);
        Director::getInstance()->getRenderer()->addCommand(&_command);
    }
    
    void OpenGLMatrix::onDraw()
    {
        //获取当前node 的shader
        auto glprogram = getGLProgram();
        //需要在init中指定shader才能在这use
        glprogram->use();
        
        //绑定纹理, 它会自动把纹理赋值给片段着色器texture.fsh的采样器u_myTexture.因为一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
    //    GL::bindTexture2D(textureId);
        
        /
        //纹理单元, uniform采样器对应纹理单元
        GL::bindTexture2DN(0, textureId);
        glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture1"), 0);
        GL::bindTexture2DN(1, textureId2);
        glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture2"), 1);
        /
        //矩阵变换,给vertex shader传值
        GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
        glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model->m);
        GLuint viewLoc = glGetUniformLocation(glprogram->getProgram(), "view");
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, view->m);
        GLuint projectionLoc = glGetUniformLocation(glprogram->getProgram(), "projection");
        glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, projection->m);
        
        
        glBindVertexArray(vao);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        glBindVertexArray(0);
    }
    


    3D立方体

    要想渲染一个立方体,我们一共需要36个顶点(6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点)。注意,这一次我们省略了颜色值,因为我们只通过纹理来获取最终的颜色值。这36个顶点的位置:
    GLfloat vertices[] = {
            //3维顶点,            //纹理坐标
            -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
        };
    

    我们使用glDrawArrays来绘制立方体,但这一次总共有36个顶点:
    glDrawArrays(GL_TRIANGLES, 0, 36);

    注意,这里需要开启深度测试。OpenGL存储深度信息在一个叫做Z缓冲(Z-buffer)的缓冲中,它允许OpenGL决定何时覆盖一个像素而何时不覆盖。通过使用Z缓冲,我们可以配置OpenGL来进行深度测试。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。深度测试默认是关闭的。
        //开启深度测试
        Director::getInstance()->setDepthTest(true);

    更多的立方体

    让我们为每个立方体定义一个位移向量来指定它在世界空间的位置。我们将在一个glm::vec3数组中定义n个立方体位置:

        cubpos[0] = {0, 0, 0};
        cubpos[1] = {2, 5, -15};
        cubpos[2] = {-1, -2, -2};
        cubpos[3] = {1, 0.5, -1.6};

    在游戏循环中,我们调用glDrawArrays n次,但这次在我们渲染之前每次传入一个不同的模型矩阵到顶点着色器中。我们将会在游戏循环中创建一个小的循环用不同的模型矩阵渲染我们的物体n次。注意我们也对每个箱子加了一点旋转:

    for (int i=0; i<4; i++) { //画n个立方体
            Mat4* posmodel = new Mat4();
            posmodel->translate(cubpos[i]);
            posmodel->rotate(Vec3(0, 1, 0), CC_DEGREES_TO_RADIANS(50*i));
            GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
            glUniformMatrix4fv(modelLoc, 1, GL_FALSE, posmodel->m);
            glDrawArrays(GL_TRIANGLES, 0, 36);
        }

    全部代码:
    //
    //  Triangle.cpp
    //  shaderTest
    //
    //  Created by MacSBL on 2016/12/15.
    //
    //
    
    #include "OpenGLCube.h"
    
    
    bool OpenGLCube::init()
    {
        if (!Layer::init()) {
            return false;
        }
        
        GLfloat vertices[] = {
            //3维顶点,            //纹理坐标
            -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
        };
        
        //创建并绑定纹理
        //
        //方法1
        auto sprite = Sprite::create("awesomeface.png");
        textureId2 = sprite->getTexture()->getName();
        //
        //方法2
        glGenTextures(1, &textureId);
        glBindTexture(GL_TEXTURE_2D, textureId);
        
        //纹理环绕方式
        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_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        
        //加载纹理
        Image* img = new Image();
        img->initWithImageFile("wall.jpg");
        int width = img->getWidth();
        int height = img->getHeight();
        unsigned char* imgdata = img->getData();
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imgdata);
        glGenerateMipmap(GL_TEXTURE_2D);
        //释放内存,解绑texture
        CC_SAFE_DELETE(img);
        glBindTexture(GL_TEXTURE_2D, 0);
        ///
        
        //1、绑定vao
        //顶点数组对象(Vertex Array Object)被绑定后,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        
        // 2. 把顶点数组复制到缓冲(vbo)中供OpenGL使用
        //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
        GLuint vbo;
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        //把之前定义的顶点数据复制到缓冲的内存中
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        
        
        //解析顶点数据, 应用到第1个顶点属性上
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5* sizeof(GLfloat), (GLvoid*)0);
        glEnableVertexAttribArray(0);
        // 纹理坐标,应用到第2个属性上
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
        glEnableVertexAttribArray(1);
        
        //解除绑定vao和vbo
        glBindVertexArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        
        //——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
        auto program = new GLProgram;
        program->initWithFilenames("cube.vsh", "cube.fsh");
        program->link();
        
        
        //——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————-
        //矩阵
        //绕某轴旋转x°
        model = new Mat4();
        model->rotate(Vec3(1, 0, 0), CC_DEGREES_TO_RADIANS(50));
        
        //离我们有一些距离
        view = new Mat4();
        view->translate(0, 0, -3);
        
        //有透视效果(顶点越远,变得越小)
        cocos2d::Size viewsize = Director::getInstance()->getVisibleSize();
        projection = new Mat4();
        Mat4::createPerspective(45, viewsize.width/viewsize.height, 0.1, 200, projection);
        
        
        this->setGLProgram(program);
        //开启深度测试
        Director::getInstance()->setDepthTest(true);
        
        cubpos[0] = {0, 0, 0};
        cubpos[1] = {2, 5, -15};
        cubpos[2] = {-1, -2, -2};
        cubpos[3] = {1, 0.5, -1.6};
        
        return true;
    }
    
    void OpenGLCube::visit(cocos2d::Renderer *renderer, const cocos2d::Mat4 &parentTransform, uint32_t parentFlags)
    {
        Layer::visit(renderer, parentTransform, parentFlags);
        _command.init(_globalZOrder);
        _command.func = CC_CALLBACK_0(OpenGLCube::onDraw, this);
        Director::getInstance()->getRenderer()->addCommand(&_command);
    }
    
    void OpenGLCube::onDraw()
    {
        //获取当前node 的shader
        auto glprogram = getGLProgram();
        //需要在init中指定shader才能在这use
        glprogram->use();
        
        //绑定纹理, 它会自动把纹理赋值给片段着色器texture.fsh的采样器u_myTexture.因为一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
    //    GL::bindTexture2D(textureId);
        
        /
        //纹理单元, uniform采样器对应纹理单元
        GL::bindTexture2DN(0, textureId);
        glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture1"), 0);
        GL::bindTexture2DN(1, textureId2);
        glUniform1i(glGetUniformLocation(glprogram->getProgram(), "u_myTexture2"), 1);
        /
        //矩阵变换,给vertex shader传值
    //    model->rotate(Vec3(1, 0, 0), CC_DEGREES_TO_RADIANS(2));
    //    GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
    //    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model->m);
        GLuint viewLoc = glGetUniformLocation(glprogram->getProgram(), "view");
        glUniformMatrix4fv(viewLoc, 1, GL_FALSE, view->m);
        GLuint projectionLoc = glGetUniformLocation(glprogram->getProgram(), "projection");
        glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, projection->m);
        
        
        glBindVertexArray(vao);
        for (int i=0; i<4; i++) { //画n个立方体
            Mat4* posmodel = new Mat4();
            posmodel->translate(cubpos[i]);
            posmodel->rotate(Vec3(0, 1, 0), CC_DEGREES_TO_RADIANS(50*i));
            GLuint modelLoc = glGetUniformLocation(glprogram->getProgram(), "model");
            glUniformMatrix4fv(modelLoc, 1, GL_FALSE, posmodel->m);
            glDrawArrays(GL_TRIANGLES, 0, 36);
        }
        glBindVertexArray(0);
    }
    







    展开全文
  • OpenGL坐标

    2012-10-17 15:06:00
    我们已定义好了多边形,下面就要了解如和使用OpenGL ES的API来绘制(渲染)这个多边形了。OpenGL ES提供了两类方法来绘制一个空间几何图形: public abstract void glDrawArrays(int mode, int first, int count)...

    我们已定义好了多边形,下面就要了解如和使用OpenGL ES的API来绘制(渲染)这个多边形了。OpenGL ES提供了两类方法来绘制一个空间几何图形:

    • public abstract void glDrawArrays(int mode, int first, int count)   使用VetexBuffer 来绘制,顶点的顺序由vertexBuffer中的顺序指定。
    • public abstract void glDrawElements(int mode, int count, int type, Buffer indices)  ,可以重新定义顶点的顺序,顶点的顺序由indices Buffer 指定。

    前面我们已定义里顶点数组,因此我们将采用glDrawElements 来绘制多边形。

    同样的顶点,可以定义的几何图形可以有所不同,比如三个顶点,可以代表三个独立的点,也可以表示一个三角形,这就需要使用mode 来指明所需绘制的几何图形的基本类型。

     

    定义面的顶点的顺序很重要,其顺序(逆时针或顺时针)决定面的正面和反面

    逆时针是正面,顺时针是反面(GL10.GL_CCW模式下,否则相反),貌似只和顺序有关,和哪个点在前面无关。 如果开启了反面不渲染,则顺时针表示的部分(反面)将不显示.

    OpenGL ES 使用也只能使用三角形来定义一个面(Face),为了获取绘制的高性能,一般情况不会同时绘制面的前面和后面,
    只绘制面的“前面”。虽然“前面”“后面”的定义可以应人而易,但一般为所有的“前面”定义统一的顶点顺序(顺时针或是逆时针方向)。
    只绘制“前面”的过程称为”Culling”。

    下面代码设置逆时针方法为面的“前面”:
    gl.glFrontFace(GL10.GL_CCW);

    打开 忽略“后面”设置:
    gl.glEnable(GL10.GL_CULL_FACE);

    明确指明“忽略“哪个面的代码如下:
    gl.glCullFace(GL10.GL_BACK);

     

    下图中定义了四个顶点和对应的Android 顶点定义:

    private float vertices[] = {
     -1.0f,  1.0f, 0.0f,  // 0, Top Left
     -1.0f, -1.0f, 0.0f,  // 1, Bottom Left
     1.0f, -1.0f, 0.0f,  // 2, Bottom Right
     1.0f,  1.0f, 0.0f,  // 3, Top Right
    };

     

     

     

     

    UV Mapping指将Bitmap的像素映射到Mesh上的顶点。

    UV坐标定义为左上角(0,0),右下角(1,1)(因为使用的2D Texture),下图坐标显示了UV坐标,右边为我们需要染色的平面的顶点顺序:

     为了能正确的匹配,需要把UV坐标中的(0,1)映射到顶点0,(1,1)映射到顶点2等等。

    float textureCoordinates[] = {0.0f, 1.0f,
     1.0f, 1.0f,
     0.0f, 0.0f,
     1.0f, 0.0f };

     

     

    如果使用如下坐标定义:

    float textureCoordinates[] = {0.0f, 0.5f,
     0.5f, 0.5f,
     0.0f, 0.0f,
     0.5f, 0.0f };

    Texture匹配到Plane的左上角部分。

    float textureCoordinates[] = {0.0f, 2.0f,
     2.0f, 2.0f,
     0.0f, 0.0f,
     2.0f, 0.0f };

    将使用一些不存在的Texture去渲染平面(UV坐标为0,0-1,1 而 (0,0)-(2,2)定义超过UV定义的大小),这时需要告诉OpenGL库如何去渲染这些不存在的Texture部分。

     

     

     

    转载于:https://www.cnblogs.com/zijianlu/archive/2012/10/17/2727757.html

    展开全文
  • OpenGL坐标变换

    千次阅读 2018-08-16 19:03:52
    基础概述 众所周知,OpenGL是一个3D图形库,在终端设备上广泛使用。但是我们的显示设备都是2D平面,那么OpenGL怎么把3D...通过OpenGL坐标变换,我们可以在一个给定的观察视角下,把3D物体投影到2D屏幕上,再经过后...

    基础概述

    众所周知,OpenGL是一个3D图形库,在终端设备上广泛使用。但是我们的显示设备都是2D平面,那么OpenGL怎么把3D图形映射到2D屏幕那?这就是OpenGL坐标变换所要完成的工作。 
    一般情况下,我们总是通过一个2D屏幕,观察3D世界。因此,我们实际看到的是3D世界在2D屏幕上的一个投影。通过OpenGL坐标变换,我们可以在一个给定的观察视角下,把3D物体投影到2D屏幕上,再经过后面的光栅化和片元着色,整个3D物体就映射成了2D屏幕上的像素。 
    OpenGL的坐标变换流程如下所示: 
    OpenGL坐标变换过程 
    我们先简单看下整个流程: 
    1. 首先,输入顶点一般是以本地坐标表示的3D模型。本地坐标是为了研究孤立的3D模型,坐标原点一般都是模型的中心。每个3D模型都有自己的本地坐标系(Local Coordinate),互相之间没有关联。 
    2. 当我们需要同时渲染多个3D物体时,需要把不同的3D模型,变换到一个统一的坐标系,这就是世界坐标系(World Coordinate)。把物体从本地坐标系变换到世界坐标系,是通过一个Model矩阵完成的。模型矩阵可以实现多种变换:平移(translation)、缩放(scale)、旋转(rotation)、镜像(reflection)、错切(shear)等。例如:通过平移操作,我们可以在世界坐标系的不同位置绘制同一个3D模型; 
    3. 世界坐标系中的多个物体共同构成了一个3D场景。从不同的角度观察这个3D场景,我们可以看到不同的屏幕投影。OpenGL提出了摄像头坐标系的概念,即从摄像头位置来观察整个3D场景。把物体从世界坐标系变换到摄像头坐标系,是通过一个View矩阵完成的。视图矩阵定义了摄像头的位置、方向向量和上向量等构成摄像头坐标系的基础信息。View矩阵左乘世界坐标系中顶点A的坐标,就把顶点A变换到了摄像头坐标系。同一个3D物体,在世界坐标系中,拥有一个世界坐标;在摄像头坐标系中,拥有一个摄像头坐标,View变换就是负责把物体的坐标从世界坐标系变换到摄像头坐标系。 
    4. 因为我们是从一个2D屏幕观察3D场景,而屏幕本身不是无限大的。所以当从摄像头的角度观察3D场景时,可能无法看到整个场景,这时候就需要把看不到的场景裁减掉。投影变换就是负责裁剪工作,投影矩阵指定了一个视见体(View Frustum),在视见体内部的物体会出现在投影平面上,而在视见体之外的物体会被裁减掉。投影包括很多类型,OpenGL中主要考虑透视投影(Perspective Projection)和正交投影(Orthographic Projection),两者的区别在后面会详细介绍。除此之外,通过Projection矩阵,可以把物体从摄像头坐标系变换到裁剪坐标系。在裁剪坐标下,X、Y、Z各个坐标轴上会指定一个可见范围,超过可见范围的顶点(vertex)都会被裁剪掉。 
    5. 每个裁剪坐标系指定的可见范围可能是不同的,为了得到一个统一的坐标系,需要对裁剪坐标进行透视除法(Perspective Division),得到NDC坐标(Normalized Device Coordinates - 标准化设备坐标系)。透视除法就是将裁剪坐标除以齐次分量W,得到NDC坐标:

    在NDC坐标系中,X、Y、Z各个坐标轴的区间是[-1,1]。因此,可以把NDC坐标系看做作一个边长为2的立方体,所有的可见物体都在这个立方体内部。 
    6. NDC坐标系的范围是[-1,1],但是我们的屏幕尺寸是千变万化的,那么OpenGL是如何把NDC坐标映射到屏幕坐标的那?视口变换(Viewport Transform)就是负责这块工作的。在OpenGL中,我们只需要通过glViewport指定绘制区域的坐标和宽高,系统会帮我们自动完成视口变换。经过视口变换,我们就得到了2D屏幕上的屏幕坐标。需要注意的是:屏幕坐标与屏幕的像素位置是不一样的,屏幕坐标是屏幕上任意一个顶点的精确位置,可以是任意小数。但是像素位置只能是整数(具体的某个像素)。这里的视口变换是从NDC坐标变换到屏幕坐标,还没有生成最终的像素位置。从屏幕坐标映射到对应的像素位置,是后面光栅化完成的。

     

    在OpenGL中, 本地坐标系、世界坐标系和摄像头坐标系都属于右手坐标系,而最终的裁剪坐标系和标准化设备坐标系属于左手坐标系。 
    左右手坐标系的示意图如下所示,其中大拇指、食指、其余手指分别指向x,y,z轴的正方向。 

    下面我们分别来看下模型变换、视图变换、投影变换和视口变换的推导和使用。

    模型变换

    模型变换通过对3D模型执行平移、缩放、旋转、镜像、错切等操作,来调整模型在世界坐标系中的位置。模型变换是通过模型矩阵来完成的,我们看下每种模型矩阵的推导过程。

    平移变换

    平移就是将一个顶点A = (x,y,z),移动到另一个位置 =(,,),移动距离D =  - A = ( - x , - y , - z) = ( ,  , ),所以可以用顶点A来表示: 

    ()


    通过平移矩阵来表示如下所示: 

    其中就是平移变换矩阵,表示X轴上的位移,表示Y轴上的位移,表示Z轴上的位移。 
    虽然看上去很繁琐,但是在OpenGL中,我们可以通过GLM库来实现平移变换。

     

    
     
    1. glm::mat4 model; // 定义单位矩阵
    2. model = glm::translate(model, glm::vec3(1.0f, 1.0f, 1.0f));

    上述代码定义了平移模型矩阵,表示在X、Y、Z轴上同时位移1。

    缩放变换

    可以在X、Y和Z轴上对物体进行缩放,3个坐标轴相互独立。对于以原点为中心的缩放,假设顶点A(x,y,z)在X、Y和Z轴上分别放大、、倍,那么可以得到放大后的顶点 =( * x , * y ,  * z),通过缩放矩阵来表示如下所示: 


    其中就是缩放变换矩阵。 
    默认情况下,缩放的中心点是坐标原点,如果我们要以指定顶点P( ,  , )为中心对物体进行缩放。那么可以按照如下步骤操作: 
    1. 把顶点P移动到坐标原点 
    2. 以坐标原点为中心,旋转指定角度 
    3. 把顶点P移动回原来的位置 
    整个过程可以简化成一个矩阵: 

    在OpenGL中,我们可以通过GLM库来实现缩放变换:

     

    
     
    1. glm::mat4 model; // 定义单位矩阵
    2. model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f);

    上述代码定义了缩放模型矩阵,表示在X轴上fa2倍,Y轴上缩小0.5倍、Z轴上保持不变。

    旋转变换

    在3D空间中,旋转需要定义一个旋转轴和一个角度。物体会沿着给定的旋转轴旋转指定角度。 
    我们首先看下,沿着Z轴旋转的旋转矩阵是怎样的? 
    假设有一个顶点P,原始坐标为 ( ,  , ),离原点的距离是,沿着Z轴顺时针旋转度,新的坐标为( ,  , ),如下所示: 
    围绕Z轴旋转 
    因为旋转前后,z坐标不变,所以暂时忽略,那么可以得到: 





    根据上述公式,可以得到围绕Z轴的旋转矩阵: 

    同理,可以得到围绕X轴的旋转矩阵: 

    同理,可以得到围绕Y轴的旋转矩阵: 

     

    在OpenGL中,我们可以通过GLM库来实现旋转变换:

    
     
    1. glm::mat4 model; // 定义单位矩阵
    2. model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(0.4f, 0.6f, 0.8f));

    上述代码表示:围绕向量(0.4f, 0.6f, 0.8f),顺时针旋转45度。

    在进行旋转操作时,经常有一个困惑:顺时针是正方向,还是逆时针是正方向? 
    其实,存在一个左手规则和右手规则,可以用于判断物体绕轴旋转时的正方向。 
    左手规则和右手规则 
    在OpenGl中,我们使用右手规则,大拇指指向旋转轴的正方向,其余手指的弯曲方向即为旋转正方向。所以上面的-45度是顺时针旋转。

    模型变换的顺序问题

    因为矩阵不满足交换律,所以平移、旋转和缩放的顺序十分重要, 
    一般是先缩放、再旋转、最后平移。当然最终还是要考虑实际情况。 
    还有一点需要注意,GLM操作矩阵的顺序和实际效果是相反的。如下所示,虽然书写顺序是:平移、旋转和缩放,但是实际最终的模型矩阵是:先缩放、再旋转、最后平移。

    
     
    1. glm::mat4 model; // 定义单位矩阵
    2. model = glm::translate(model, glm::vec3(1.0f, 1.0f, 1.0f));
    3. model = glm::rotate(model, glm::radians(-45.0f), glm::vec3(0.4f, 0.6f, 0.8f));
    4. model = glm::scale(model, glm::vec3(2.0f, 0.5f, 1.0f);

    视图变换

    经过模型变换,都有的坐标都处于世界坐标系中,本节就是以摄像头的角度观察整个世界空间。首先需要定义一个摄像头坐标系。 
    一般情况下,定义一个坐标系需要以下参数: 
    1. 指定坐标系的维度:2D、3D、4D等。 
    2. 定义坐标空间的轴向量,例如:X轴、Y轴、Z轴,这些向量称为基向量,基向量一般都是正交的。坐标系中的所有顶点都是通过基向量表示的。 
    3. 坐标系的原点O,原点是坐标系中所有其他点的参考点。 
    简单来说,坐标系=(基向量,原点O)

    同一个顶点,在不同的坐标系中拥有不同的坐标,那怎么才能把世界坐标系中的顶点坐标,变换到摄像头坐标系那? 
    要实现不同坐标系之间的坐标转换,需要计算一个变换矩阵。这个矩阵就是坐标系A中的原点和基向量在另一个坐标系B下的坐标表示。假设存在A坐标系B坐标系以及顶点V,那么顶点V在A和B坐标系下的坐标变换公式如下所示: 

     

     

    简单解释一下:

    
     
    1. 顶点V在A坐标系的坐标 = B坐标系的基向量和原点在A坐标系下的坐标表示构成的变换矩阵 * 顶点V在B坐标系的坐标;
    2.  
    3. 顶点V在B坐标系的坐标 = A坐标系的基向量和原点在B坐标系下的坐标表示构成的变换矩阵 * 顶点V在A坐标系的坐标

    其中,和互为逆矩阵。所以坐标系之间的切换,关键就是求出坐标系之间互相表示的变换矩阵。那么矩阵应该怎么计算那?假设坐标系A的三个基向量和原点在B坐标空间的单位坐标向量分别是、、和,那么矩阵如下所示: 


    矩阵的计算方式也类似,此处不再赘述。

     

    下面我们看下OpenGL的视图变换矩阵是怎么计算出来的? 
    现在存在两个坐标系:世界坐标系W和摄像头坐标系E,还有一个顶点V,并且知道顶点V在世界坐标系的坐标 = (,,),那么顶点V在摄像头坐标系下的坐标是多少那?根据上面的公式可知,我们首先需要计算出矩阵。

    众所周知,世界坐标系的原点O = (0,0,0),三个基向量分别是,X轴:(1,0,0)、Y轴:(0,1,0)、Z轴:(0,0,1)。 
    理论上,定义一个摄像头坐标系,需要4个参数: 
    1. 摄像头在世界坐标系中的位置(摄像头坐标系的原点) 
    2. 摄像头的观察方向(摄像头坐标系的Z基向量) 
    3. 一个指向摄像头右侧的向量(摄像头坐标系的X基向量) 
    4. 一个指向摄像头上方的向量(摄像头坐标系的Y基向量)。

    通过上述4个参数,我们实际上创建了一个三个单位轴相互垂直的,以摄像机位置为原点的坐标系。 
    摄像头坐标系

    在使用过程中,我们只需要指定3个参数: 
    1. 摄像机位置向量() 
    2. 摄像机指向的目标位置向量() 
    3. 指向摄像头上方的向量()

    接下来是根据上面3个参数,推导出摄像头坐标系单位基向量的步骤: 
    1. 首先计算摄像头的方向向量(方向向量是摄像头坐标系的Z轴正方向,和实际的观察方向是相反的)。 


    然后计算出单位方向向量 

    2. 根据上向量和单位方向向量确定摄像头的右向量 

    然后计算出单位右向量 

    3. 根据单位右向量和单位方向向量确定单位上向量 

     

    这样,就确定了摄像头坐标系的三个单位基向量:、和以及摄像头的位置向量。这四个参数一起确定了摄像头坐标系:摄像头位置是坐标原点,单位右向量指向正X轴,单位上向量指向正Y轴,单位方向向量指向正Z轴。

    现在我们已经定义了一个摄像头坐标系,下一步就是把世界坐标系中的顶点V = (,,),变换到这个摄像头坐标系。根据上文可知,顶点V在摄像头坐标系E的坐标计算过程如下所示: 


    所以关键是计算变换矩阵,而根据摄像头坐标系的基向量和原点在世界空间中的坐标表示,我们可以得到: 

    那么最终的变换矩阵如下所示: 

    其中,dot函数表示向量的点积,是一个标量。最终,顶点V在摄像头坐标系下的坐标如下所示: 

     

    上面的矩阵就是View变换矩阵。

    下面看一个案例:假设摄像头的坐标是(0, 0, 3),摄像头的观察方向是世界坐标系的原点(0,0,0),上向量是(0,1,0),顶点V在世界坐标系的坐标为(1,1,0),那么可以计算出摄像头坐标系的基向量和原点如下所示: 
    1.  =  
    2.  =  
    3.  =  
    4.  =  
    所以对应的View变换矩阵就是: 

     

    最后,顶点V在摄像头坐标系的坐标就是: 

     

    虽然上述流程很复杂,但在OpenGL中,我们可以通过GLM库定义View矩阵。针对上述案例,通过lookAt函数就可以得到View矩阵。

    
     
    1. glm::mat4 view;
    2. 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));

    经过验证,通过lookAt函数得到View矩阵为: 

     

    很显然,通过lookAt函数得到View矩阵和上面我们推导的View矩阵是一致的。

    投影变换

    前面经过模型变换和视图变换后,3D模型已经处于摄像头坐标系中。本节的投影变换将物体从摄像头坐标系变换到裁剪坐标系,为下一步的视口变换做好准备。 
    投影变换通过指定视见体来决定场景中哪些物体可以呈现在屏幕上。在视见体中的物体会出现在投影平面上,而在视见体之外的物体不会出现在投影平面上。在OpenGL中,我们主要考虑透视投影和正交投影,两者的区别如下所示: 
    透视投影和正交投影
    上图中,红色和黄色球在视见体内,因而呈现在投影平面上;绿色球在视见体外,所以没有投影到近平面上。除此之外,透视投影会根据物体的Z坐标,决定物体在投影平面的大小,原则是:远小近大,符合生活常识。而正交投影不考虑物体Z坐标,所有物体在投影平面上保持原来的大小。

    不管透视投影,还是正交投影,都可以通过指定(GLdouble left, GLdouble right, GLdouble bottom, GLdouble top, GLdouble near, GLdouble far)6个参数来指定视见体。(left,bottom)指定了近裁剪面左下角的坐标,(right,top)指定了近裁剪面右上角的坐标,-near表示近裁剪面,−far表示远裁剪面。下面需要利用这6个参数,推导投影矩阵。

    在摄像头坐标系下,摄像头指向-z轴,所以近裁剪面z=−near,远裁剪面z=−far。并且OpenGL是在近平面上成像的。

    通过上述6个参数指定的透视投影变换如下所示: 
    透视投影变换

    通过上述6个参数指定的正交投影变换如下所示: 
    正交投影变换

    投影变换和透视除法后,摄像头坐标系中的顶点被映射到一个标准立方体中,即NDC坐标系。其中X轴上:[left,right]映射到[−1,1],Y轴上:[bottom,top]映射到[-1,1]中,Z轴上:[near,far]映射到[−1,1],下面的矩阵推导会利用这里的映射关系。下面我们分别看下两种投影矩阵的推导过程。

    透视投影

    透视投影和透视除法的坐标映射如下所示: 
    投影映射关系

    上图中,摄像头坐标系是右手坐标系,NDC是左手坐标系,NDC坐标系的Z轴指向摄像头坐标系的-Z轴方向。

    假设顶点V在摄像头坐标系的坐标 = ( ,  ,  , ),变换到裁剪坐标系的坐标 = ( ,  ,  , ),透视除法到NDC坐标系的坐标 = ( ,  ,  , )。我们的目标是计算出投影矩阵,使得: 


    同时,可得到透视除法的变换: 

    首先,我们看下投影矩阵对X轴和Y轴的变换。顶点P投影到近平面后,得到顶点 = ( ,  , −near)。具体示意图如下所示: 
    X轴映射 Y轴映射 
    利用三角形的相似性,通过左图可知: 

    所以,可以得到X轴上的投影值: 

    同理,通过右图,可以得到Y轴上的投影值: 

    由(1)(2)公式可以发现,他们都除以了分量,并且与之成反比。这可以作为透视除法的一个线索,因此我们的矩阵如下所示: 

    也就是说。

     

    接下来,我们根据、与NDC坐标的映射关系,推导出的前两行。 
    满足[left,right]映射到[-1,1],如下所示: 
    Mapping from $x_p$ to $x_n$ 
    因为是线性映射关系,所以可以设置线性方程,求出系数K和常量P。 


    通过代入[left,right]到[-1,1]的映射关系,可以得到线性方程: 

    将上面的公式(1)代入公式(3),可得: 

    又因为,所以可以进一步简化公式: 

    根据公式(5),可以进一步得到矩阵: 

     

    OK,继续看下的映射关系:满足[bottom,top]映射到[-1,1],如下所示:Mapping from $y_p$ to $y_n$ 
    同理,根据线性映射关系,可以得到如下公式: 


    又因为,所以可以进一步简化公式: 

    根据公式(7),可以进一步得到矩阵: 

    接下来需要计算的系数,这和、的计算方式不同,因为摄像头坐标系的坐标投影到近平面后总是-near。同时我们知道与x和y分量无关,因此,可进一步得到矩阵: 

    因为,所以可以得到: 

    又因为摄像头坐标系中 = 1,所以进一步得到: 

    同样的,代入与的映射关系:[-near,-far]映射到[-1,1],可得到: 

    又因为,可以进一步简化得到和的关系: 

    由公式(9)就可以知道A和B了,因此,最终的矩阵: 

    一般情况下,投影的视见体都是对称的,即满足left=−right,bottom=−top,那么可以得到: 


    则矩阵可以简化为: 

     


    除了可以通过(left,right,bottom,top,near,far)指定透视投影矩阵外,还可以通过函数glm::perspective指定视角(Fov)、宽高比(Aspect)、近平面(Near)、远平面(Far)来生成透视投影矩阵,如下所示,指定了45度视角,近平面和远平面分别是0.1f和100.0f:

    
     
    1. glm::mat4 proj = glm::perspective(glm::radians(45.0f), width/height, 0.1f, 100.0f);

    观察视角的示意图如下所示: 
    视角 
    通过视角指定的透视投影变换如下所示: 
     
    通过视角指定的透视投影矩阵的视见体是对称的: 
    透视投影矩阵的对称视见体 
    由上图可知,近平面的宽和高如下所示: 



    因为视见体是对称的,所以把公式(10)(11)代入已有的矩阵,可以得到由视角Fov表示的矩阵,如下所示: 

    通过矩阵左乘摄像头坐标系中的顶点,就把这些顶点变换到了裁剪坐标系。然后再经过透视除法,就变换到了NDC坐标系。

     

    正交投影

    相比于透视投影矩阵,正交投影矩阵要简单一些,如下所示: 
    正交投影
    因为正交投影不考虑远小近大的情况,所以正交投影矩阵的第4行始终为。

    对于正交投影变换,投影到近平面的坐标( , ) = ( , ),因此可以直接利用与、与、与的线性映射关系,求出线性方程系数。X、Y、Z轴的映射关系如下所示:

    映射关系 映射值 示意图
    与的映射关系 [left , right]  [-1 , 1] $x_e$与$x_n$的映射关系
    与的映射关系 [bottom , top]  [-1 , 1] $y_e$与$y_n$的映射关系
    与的映射关系 [near , far]  [-1 , 1] $z_e$与$z_n$的映射关系

    根据上述的映射关系,同时摄像头坐标系的 = 1,可以得到三个线性方程,如下所示: 

    和的映射关系

     

    和的映射关系

     

    和的映射关系


    根据上述3个线性方程,可以得到正交投影矩阵: 

    如果视见体是对称的,即满足left=−right,bottom=−top,那么可以得到: 


    则正交投影矩阵可以进一步简化为: 

     

    视口变换

    经过投影变换和透视除法后,我们裁减掉了不可见物体,得到了NDC坐标。最后一步是把NDC坐标映射到屏幕坐标(,  , )。如下所示: 
    NDC坐标变换到屏幕坐标 
    在映射到屏幕坐标时,我们需要指定窗口的位置、宽高和深度。如下所示:

    
     
    1. //指定窗口的位置和宽高
    2. glViewport(GLint x , GLint y , GLsizei width , GLsizei height);
    3. //指定窗口的深度
    4. glDepthRangef(GLclampf near , GLclampf far);

    那么可以NDC坐标和屏幕坐标的线性映射关系:

    映射关系 映射值
    与的映射关系 [-1 , 1]  [x , x + width]
    与的映射关系 [-1 , 1]  [y , y + height]
    与的映射关系 [-1 , 1]  [near , far]

    因此,可以设置线性方程,求出系数K和常量P。 


    把上述映射关系代入线性方程,可以得到各个分量的参数值。

     

    坐标分量 线性方程的系数K 线性方程的常量P
    X分量线性方程   x + 
    Y分量线性方程   y + 
    Z分量线性方程    

    通过上述各个坐标分量值,可以得到视口变换矩阵: 


    因此,通过ViewPort矩阵左乘NDC坐标,就得到了屏幕坐标。

     

    对于2D屏幕,nearfar一般为0。因此ViewPort矩阵的第三行都是0。所以经过视口变换后,屏幕坐标的Z值都是0。

    至此,OpenGL的整个坐标变换过程都介绍完了,关键还是要多实践、实践、实践!!!

    展开全文
  • opengl坐标系统

    2021-05-20 17:06:08
    opengl坐标系统 OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都...

    GLM

    GLM是OpenGL Mathematics的缩写,专门为OpenGL量身定做的数学库.

    opengl坐标系统

    OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

    将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样,对我们来说比较重要的总共有5个不同的坐标系统:

    • 局部空间(Local Space,或者称为物体空间(Object Space))
    • 世界空间(World Space)
    • 观察空间(View Space,或者称为视觉空间(Eye Space))
    • 裁剪空间(Clip Space)
    • 屏幕空间(Screen Space)

    这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。下面的这张图展示了整个流程以及各个变换过程做了什么:
    在这里插入图片描述

    1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
    2. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
    3. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
    4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
    5. 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

    我们为上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:
    在这里插入图片描述
    注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

    //世界坐标
    glm::mat4 model;
    model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
    
    //观察矩阵
    glm::mat4 view;
    // 注意,我们将矩阵向我们要进行移动场景的反方向移动。
    view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
    
    //投影矩阵
    glm::mat4 projection;
    projection = glm::perspective(glm::radians(45.0f), screenWidth / screenHeight, 0.1f, 100.0f);
    

    既然我们已经创建了变换矩阵,我们应该将它们传入着色器。首先,让我们在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标:

    #version 330 core
    layout (location = 0) in vec3 aPos;
    ...
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    void main()
    {
        // 注意乘法要从右向左读
        gl_Position = projection * view * model * vec4(aPos, 1.0);
        ...
    }
    

    我们还应该将矩阵传入着色器:

    int modelLoc = glGetUniformLocation(ourShader.ID, "model"));
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    ... // 观察矩阵和投影矩阵与之类似
    

    摄像机

    当我们讨论摄像机/观察空间(Camera/View Space)的时候,是在讨论以摄像机的视角作为场景原点时场景中所有的顶点坐标:观察矩阵把所有的世界坐标变换为相对于摄像机位置与方向的观察坐标。要定义一个摄像机,我们需要它在世界空间中的位置、观察的方向、一个指向它右测的向量以及一个指向它上方的向量。使用这些摄像机向量我们就可以创建一个LookAt矩阵了。
    在这里插入图片描述

    GLM已经提供了这些支持。我们要做的只是定义一个摄像机位置,一个目标位置和一个表示世界空间中的上向量的向量。接着GLM就会创建一个LookAt矩阵,我们可以把它当作我们的观察矩阵:

    glm::mat4 view;
    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));
    
    展开全文
  • openGL坐标

    2017-08-14 19:30:18
    openGL坐标
  • 屏幕坐标转OpenGl坐标

    2011-03-12 10:25:05
    屏幕坐标到opengl坐标体系的转换。详细介绍opengl的原理。
  • OpenGL中屏幕坐标转为OpenGL坐标

    千次阅读 2013-08-14 11:26:48
    在opengl中我们经常碰到需要把屏幕坐标转换成opengl坐标,例如我们做拾取,旋转,平移等操作。网上有很多相关代码:  原理:    鼠标点击屏幕中的某点,然后获取该点屏幕坐标对应的OpenGL坐标。鼠标点的坐标很...
  • OpenGL: 屏幕坐标向OpenGL坐标转换

    千次阅读 2014-08-15 23:27:13
    屏幕坐标向OpenGL坐标转换 很多人用OpenGL绘图会遇到一个问题即屏幕坐标向OpenGL坐标转换,在网上流传着如下类似的代码: 注:(x, y)是屏幕坐标,(winX, winY, winZ)是视景体坐标及深度坐标,(posX, posY, posZ是...
  • 一、 常用的坐标系 ...2. opengl坐标系中采用的是3维坐标: static final float COORD[] = { -1.0f, -1.0f, //1 1.0f, -1.0f, //2 -1.0f, 1.0f, //3 1.0f, 1.0f, //4 }; 坐标原点默...
  • OpenGL 坐标系定义

    2012-06-24 16:14:51
    OpenGL 坐标系定义
  • Cocos2D-x以OpenGL和OpenGL ES为基础,所以自然支持OpenGL坐标系。该坐标系原点在屏幕左下角,x轴向右,y轴向上。 (2)屏幕坐标系 屏幕坐标系使用的是不同的坐标系统,原点在屏幕左上角,x轴向右,y轴向下。iOS的...
  • OpenGL坐标: 注意在标准化设备坐标系中OpenGL实际上使用的是左手坐标系(投影矩阵交换了左右手)。 高中数学右手系:
  • 从屏幕坐标向OpenGL坐标要经过两步,第一步是屏幕坐标向视景体坐标转换,第二步是视景体坐标向OpenGL坐标转换。 参考资料:http://chiefman.blog.hexun.com/5400795_d.html
  • 主要介绍了Cocos2d-x学习笔记之世界坐标系、本地坐标系、opengl坐标系、屏幕坐标系,本文用代码和注释讲解了Cocos2d-x中的坐标体系,需要的朋友可以参考下
  • OpenGL 坐标系讲解

    2011-01-11 15:21:00
    OpenGL 坐标系讲解,详细讲解OpenGL坐标关系,坐标装换
  • opengl坐标

    2017-07-14 15:54:04
    在使用openGL的场景中,有世界坐标,局部坐标,纹理坐标,和屏幕坐标几种。 openGL 坐标系: 分3个轴,x,y,z 中心点为o, 箭头方向为正方向,最大与最小值为1和-1,这是经过归一化处理的。这样设计是...
  • OpenGL坐标

    2016-02-14 16:41:36
    温习OpenGL坐标系的时候竟然看到自己写的文档,都归类到CSDN上吧。 OpenGL使用的是右手笛卡尔坐标系统,Z正轴垂直屏幕向外,X正轴从左到右,Y正轴从下到上。 OpenGL中存下以下几种坐标系: 1.世界坐标系:世界...
  • OpenGL坐标系和像素坐标系

    千次阅读 2016-05-27 13:06:58
    OpenGL坐标系原点在中心,并且做了归一化处理。就是说xy轴,都是从[-1, 1]之间。而像素坐标系是同样的中心点,右上是正方向,只是没有做归一化处理,是以像素为单位的。那么,归一化的坐标系和像素坐标最大的区别...
  • OpenGL 坐标

    2015-10-07 23:03:16
    OPENGL坐标系可分为:世界坐标系和当前绘图坐标系。 世界坐标系以屏幕中心为原点(0, 0, 0)。你面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。长度单位这样来定: 窗口范围按此单位恰好是(-1,-1...
  • OPENGL坐标系可分为:世界坐标系和当前绘图坐标系。 世界坐标系以屏幕中心为原点(0, 0, 0)。你面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。长度单位这样来定: 窗口范围按此单位恰好是(-1,-1)...
  • UE4 & OpenGL坐标

    2021-04-17 15:34:09
    OpenGL坐标系 UE4 使用左手系(DX),OpenGL固定管线使用右手系,可以通过可编程的管线在OpenGL渲染管线中使用和UE4一样的左手系。 三维空间的坐标系 局部空间(Local Space,或者称为物体空间(Object Space)) 世界...
  • 1. OpenGL坐标系统概述   OpenGL希望每次顶点着色后,我们的可见顶点都为标准化设备坐标(Normalized Device Coordinate,NDC)。也就是说每个顶点的x,y,z都应该在−1到1之间,超出这个范围的顶点将是不可见的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,628
精华内容 2,251
关键字:

opengl坐标