如何在OpenGL中使用glOrtho()函数?

103

我无法理解glOrtho的用法。能否有人解释一下它的用途?

它是否用于设置x、y和z坐标范围的限制?

glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);

这意味着x、y和z的范围是从-1到1?


1
这个视频对我帮助很大。 - ViniciusArruda
3个回答

165

看一下这张图片:图像投影 enter image description here

glOrtho 命令会产生“斜视”(Oblique)投影,这是底部行中显示的。无论顶点在 z 方向上离观察者有多远,它们都不会消失到远处。

我每次需要在 OpenGL 中进行 2D 图形绘制(例如血条、菜单等),都会在窗口被调整大小时使用以下代码:

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0.0f, windowWidth, windowHeight, 0.0f, 0.0f, 1.0f);

这将把OpenGL坐标重新映射为相应的像素值(X从0到窗口宽度,Y从0到窗口高度)。请注意,我已经翻转了Y值,因为OpenGL坐标从窗口左下角开始。因此,通过翻转,我得到了一个更常规的(0,0)起点,它位于窗口左上角。

请注意,Z值被剪辑在0到1之间。因此,在指定顶点位置的Z值时要小心,如果它超出该范围,它将被剪裁。否则,如果它在该范围内,它看起来对位置没有影响,除了Z测试。


2
注意:(在Android上)即使模型只有负z值,似乎仍然需要为最终(远)参数设置正值。我进行了一个简单的三角形测试(禁用剔除),顶点位于z= -2。如果我使用glOrtho(.., 0.0f, -4.0f);..-1.0f, -3.0f)..-3.0f, -1.0f),则三角形是不可见的。为了可见,远参数必须为正2或更大;似乎近参数无关紧要。任何一个都可以工作:..0.0f, 2.0f)..-1.0f, 2.0f)..-3.0f, 2.0f)..0.0f, 1000.0f - ToolmakerSteve
16
OpenGL的糟糕教程数量之多令人惊讶。 - basickarl
1
@Kari,希望这个链接能够帮到你。>http://learnopengl.com/#!In-Practice/2D-Game/Rendering-Sprites - huahsin68
你能解释一下z范围吗? - mgouin
2
@mgouin z范围指定了Z近平面和Z远平面的位置。当您绘制几何图形时,它的Z值必须在两个Z平面内。如果它们超出了Z平面,您的几何图形将无法呈现。此外,您的渲染器仅具有一定的深度分辨率。如果您将远平面设置为距离1000个单位,并尝试绘制一个小型模型,其面之间的距离仅为0.1个单位,则OpenGL将无法提供所需的深度分辨率,您将在面之间看到Z-fighting(闪烁)。 - Mikepote
@Mikepote glOrtho 命令会产生您在底部行中看到的“斜”投影,我认为您指的是正交投影,而不是斜投影。斜投影不是正交投影的直接反面吗? - Distjoy

63
最小可运行示例
glOrtho:2D游戏中,靠近和远离的物体看起来大小相同:

enter image description here

"glFrustrum": 更加真实的3D效果,远离视角的相同物体看起来更小。

enter image description here

main.c

#include <stdlib.h>

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>

static int ortho = 0;

static void display(void) {
    glClear(GL_COLOR_BUFFER_BIT);
    glLoadIdentity();
    if (ortho) {
    } else {
        /* This only rotates and translates the world around to look like the camera moved. */
        gluLookAt(0.0, 0.0, -3.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
    }
    glColor3f(1.0f, 1.0f, 1.0f);
    glutWireCube(2);
    glFlush();
}

static void reshape(int w, int h) {
    glViewport(0, 0, w, h);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    if (ortho) {
        glOrtho(-2.0, 2.0, -2.0, 2.0, -1.5, 1.5);
    } else {
        glFrustum(-1.0, 1.0, -1.0, 1.0, 1.5, 20.0);
    }
    glMatrixMode(GL_MODELVIEW);
}

int main(int argc, char** argv) {
    glutInit(&argc, argv);
    if (argc > 1) {
        ortho = 1;
    }
    glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
    glutInitWindowSize(500, 500);
    glutInitWindowPosition(100, 100);
    glutCreateWindow(argv[0]);
    glClearColor(0.0, 0.0, 0.0, 0.0);
    glShadeModel(GL_FLAT);
    glutDisplayFunc(display);
    glutReshapeFunc(reshape);
    glutMainLoop();
    return EXIT_SUCCESS;
}

GitHub 上游

编译:

gcc -ggdb3 -O0 -o main -std=c99 -Wall -Wextra -pedantic main.c -lGL -lGLU -lglut

使用 glOrtho 运行:

./main 1

使用 glFrustrum 运行:

./main

在Ubuntu 18.10上测试过。
模式:
正交:相机是一个平面,可见体积为一个矩形:

enter image description here

棱台截面法(Frustrum):相机是一个点,可见体积是金字塔的一片切面。

enter image description here

图片来源

参数

我们始终从+z向-z看,+y朝上:

glOrtho(left, right, bottom, top, near, far)
  • left: 我们看到的最小 x
  • right: 我们看到的最大 x
  • bottom: 我们看到的最小 y
  • top: 我们看到的最大 y
  • -near: 我们看到的最小 z 值。是的,这是 near 的负数。因此,负输入意味着正 z
  • -far: 我们看到的最大 z 值。同样是负数。

架构:

图片来源

底层原理

最终,OpenGL总是“使用”:

glOrtho(-1.0, 1.0, -1.0, 1.0, -1.0, 1.0);

如果我们既不使用glOrtho也不使用glFrustrum,那么我们得到的就是这样。 glOrthoglFrustrum只是线性变换(又称矩阵乘法),使得:
  • glOrtho:将给定的3D矩形转换为默认立方体
  • glFrustrum:将给定的金字塔部分转换为默认立方体
然后将此变换应用于所有顶点。这就是我在2D中所说的意思。

图片来源

变换后的最后一步很简单:

  • 删除立方体外部的任何点(剔除):只需确保 xyz[-1, +1]
  • 忽略 z 分量,只取 xy,现在可以放入 2D 屏幕中

使用 glOrtho 时,z 被忽略,因此您可能会始终使用 0

您可能希望使用 z != 0 的原因是使精灵通过深度缓冲隐藏背景。

弃用

glOrthoOpenGL 4.5开始已经被弃用:兼容性配置文件12.1中的“FIXED-FUNCTION VERTEX TRANSFORMATIONS”已标记为红色。

因此,不要在生产中使用它。但是,了解它是获得一些OpenGL见解的好方法。

现代OpenGL 4程序在CPU上计算变换矩阵(很小),然后将矩阵和所有要转换的点提供给OpenGL,在并行情况下快速完成数千个点的矩阵乘法。

手写的顶点着色器然后显式地执行乘法,通常使用OpenGL着色语言方便的向量数据类型。

由于您明确编写着色器,因此可以根据需要调整算法。这种灵活性是更现代的GPU的主要特征,与旧GPU不同,旧GPU使用固定算法和一些输入参数,现在可以进行任意计算。另请参见:https://dev59.com/wmMm5IYBdhLWcg3wMs3R#36211337

使用明确的GLfloat transform[],它看起来会像这样:

glfw_transform.c

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#define GLEW_STATIC
#include <GL/glew.h>

#include <GLFW/glfw3.h>

static const GLuint WIDTH = 800;
static const GLuint HEIGHT = 600;
/* ourColor is passed on to the fragment shader. */
static const GLchar* vertex_shader_source =
    "#version 330 core\n"
    "layout (location = 0) in vec3 position;\n"
    "layout (location = 1) in vec3 color;\n"
    "out vec3 ourColor;\n"
    "uniform mat4 transform;\n"
    "void main() {\n"
    "    gl_Position = transform * vec4(position, 1.0f);\n"
    "    ourColor = color;\n"
    "}\n";
static const GLchar* fragment_shader_source =
    "#version 330 core\n"
    "in vec3 ourColor;\n"
    "out vec4 color;\n"
    "void main() {\n"
    "    color = vec4(ourColor, 1.0f);\n"
    "}\n";
static GLfloat vertices[] = {
/*   Positions          Colors */
     0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
    -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
     0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f
};

/* Build and compile shader program, return its ID. */
GLuint common_get_shader_program(
    const char *vertex_shader_source,
    const char *fragment_shader_source
) {
    GLchar *log = NULL;
    GLint log_length, success;
    GLuint fragment_shader, program, vertex_shader;

    /* Vertex shader */
    vertex_shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
    glCompileShader(vertex_shader);
    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
    glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &log_length);
    log = malloc(log_length);
    if (log_length > 0) {
        glGetShaderInfoLog(vertex_shader, log_length, NULL, log);
        printf("vertex shader log:\n\n%s\n", log);
    }
    if (!success) {
        printf("vertex shader compile error\n");
        exit(EXIT_FAILURE);
    }

    /* Fragment shader */
    fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
    glCompileShader(fragment_shader);
    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
    glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &log_length);
    if (log_length > 0) {
        log = realloc(log, log_length);
        glGetShaderInfoLog(fragment_shader, log_length, NULL, log);
        printf("fragment shader log:\n\n%s\n", log);
    }
    if (!success) {
        printf("fragment shader compile error\n");
        exit(EXIT_FAILURE);
    }

    /* Link shaders */
    program = glCreateProgram();
    glAttachShader(program, vertex_shader);
    glAttachShader(program, fragment_shader);
    glLinkProgram(program);
    glGetProgramiv(program, GL_LINK_STATUS, &success);
    glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length);
    if (log_length > 0) {
        log = realloc(log, log_length);
        glGetProgramInfoLog(program, log_length, NULL, log);
        printf("shader link log:\n\n%s\n", log);
    }
    if (!success) {
        printf("shader link error");
        exit(EXIT_FAILURE);
    }

    /* Cleanup. */
    free(log);
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);
    return program;
}

int main(void) {
    GLint shader_program;
    GLint transform_location;
    GLuint vbo;
    GLuint vao;
    GLFWwindow* window;
    double time;

    glfwInit();
    window = glfwCreateWindow(WIDTH, HEIGHT, __FILE__, NULL, NULL);
    glfwMakeContextCurrent(window);
    glewExperimental = GL_TRUE;
    glewInit();
    glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
    glViewport(0, 0, WIDTH, HEIGHT);

    shader_program = common_get_shader_program(vertex_shader_source, fragment_shader_source);

    glGenVertexArrays(1, &vao);
    glGenBuffers(1, &vbo);
    glBindVertexArray(vao);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    /* Position attribute */
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    /* Color attribute */
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);
    glBindVertexArray(0);

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(shader_program);
        transform_location = glGetUniformLocation(shader_program, "transform");
        /* THIS is just a dummy transform. */
        GLfloat transform[] = {
            0.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f,
        };
        time = glfwGetTime();
        transform[0] = 2.0f * sin(time);
        transform[5] = 2.0f * cos(time);
        glUniformMatrix4fv(transform_location, 1, GL_FALSE, transform);

        glBindVertexArray(vao);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glBindVertexArray(0);
        glfwSwapBuffers(window);
    }
    glDeleteVertexArrays(1, &vao);
    glDeleteBuffers(1, &vbo);
    glfwTerminate();
    return EXIT_SUCCESS;
}

GitHub 上游

编译和运行:

gcc -ggdb3 -O0 -o glfw_transform.out -std=c99 -Wall -Wextra -pedantic glfw_transform.c -lGL -lGLU -lglut -lGLEW -lglfw -lm
./glfw_transform.out

输出:

enter image description here

glOrtho的矩阵非常简单,只由缩放和平移组成:

scalex, 0,      0,      translatex,
0,      scaley, 0,      translatey,
0,      0,      scalez, translatez,
0,      0,      0,      1

如上所述,OpenGL 2文档中提到。 glFrustum矩阵的手动计算并不太难,但会变得很麻烦。请注意,不能像glOrtho那样仅用缩放和平移来构建截锥体,更多信息请参见:https://gamedev.stackexchange.com/a/118848/25171 GLM OpenGL C++数学库是计算此类矩阵的流行选择。http://glm.g-truc.net/0.9.2/api/a00245.html记录了orthofrustum操作。

1
“应该使用什么替代方法?” - 构建您自己的矩阵并直接赋值。 - Kromster
我尝试编译你最后的代码示例(转换三角形),但遇到了困难。我已经克隆了存储库,但是仍然出现错误 common.h:19:23: error: ‘TIME_UTC’ undeclared (first use in this function) timespec_get(&ts, TIME_UTC); - user2188550
1
@Ivanzinho 我在Ubuntu 20.04上无法重现,可能是因为它使用的是C11,而你的GCC尚未实现。但现在我已经在这个答案中最小化了示例,没有使用common.h,所以应该可以工作 :-) - Ciro Santilli OurBigBook.com

4
glOrtho描述了一种产生平行投影的变换。当前矩阵(参见glMatrixMode)与此矩阵相乘,结果替换当前矩阵,就好像使用以下矩阵作为其参数调用glMultMatrix:

OpenGL文档(我加粗了)

这些数字定义了剪裁平面的位置(左、右、下、上、近和远)。

“正常”的投影是一种提供深度幻觉的透视投影。维基百科 将平行投影定义为:

平行投影具有现实世界和投影平面上都平行的投影线。

平行投影对应于具有假想视点的透视投影,例如摄像机与物体之间距离无限远,焦距无限长或“缩放”的情况。


嗨,感谢提供信息。 我还不太理解平行投影和透视投影之间的区别。我在谷歌上搜索了一下,在http://wiki.answers.com/Q/What_is_the_difference_between_orthogonal_and_perspective_projection找到了答案。 - ufk
6
很不幸,你从answers.com得到的信息几乎没有用。例如,等轴测投影非常具有三维效果,但它是一种没有透视的平行投影。在这里可以查看更多示例以及其他投影类型的链接:http://en.wikipedia.org/wiki/Isometric_projection。 - Ben Voigt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接