Pygame + PyOpenGL 中使用不同纹理的天空盒渲染不一致

Inconsistent skybox rendering using different textures in Pygame + PyOpenGL

受我的 incomplete answer to this question, I am implementing a simple skybox in PyOpenGL in accordance with this 教程的启发,根据需要对 OpenGL 2.1/GLSL 120 和 python2.7-isms 进行了细微调整。在大多数情况下,它可以成功运行,但取决于我传递给立方体贴图的六张图像,这些图像最终要么在一对相对的面之间交换,要么随机旋转!下面是这个演示的主要 class:

import pygame
import sys
import time
import glob
import numpy as np
from ctypes import *
from OpenGL.GL import *
from OpenGL.GL import shaders
from OpenGL.GLU import *

def load_shaders(vert_url, frag_url):
    vert_str = "\n".join(open(vert_url).readlines())
    frag_str = "\n".join(open(frag_url).readlines())
    vert_shader = shaders.compileShader(vert_str, GL_VERTEX_SHADER)
    frag_shader = shaders.compileShader(frag_str, GL_FRAGMENT_SHADER)
    program = shaders.compileProgram(vert_shader, frag_shader)
    return program

def load_cubemap(folder_url):
    tex_id = glGenTextures(1)
    face_order = ["right", "left", "top", "bottom", "back", "front"]
    """
    #hack that fixes issues for ./images1/
    face_order = ["right", "left", "top", "bottom", "front", "back"]
    """
    face_urls = sorted(glob.glob(folder_url + "*"))
    glActiveTexture(GL_TEXTURE0)
    glBindTexture(GL_TEXTURE_CUBE_MAP, tex_id)
    for i, face in enumerate(face_order):
        face_url = [face_url for face_url in face_urls if face in face_url.lower()][0]
        face_image = pygame.image.load(face_url).convert()
        """
        #hack that fixes issues for ./images2/
        if face == "bottom":
            face_image = pygame.transform.rotate(face_image, 270)
        if face == "top":
            face_image = pygame.transform.rotate(face_image, 90)
        """
        """
        #hack that fixes issues for ./images3/
        if face == "bottom" or face == "top":
            face_image = pygame.transform.rotate(face_image, 180)
        """
        face_surface = pygame.image.tostring(face_image, 'RGB')
        face_width, face_height = face_image.get_size()
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, face_width, face_height, 0, GL_RGB, GL_UNSIGNED_BYTE, face_surface)
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE)
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0)
    return tex_id

def render():
    global width, height, program
    global rotation, cubemap

    glEnable(GL_DEPTH_TEST)
    glEnable(GL_TEXTURE_2D)
    glEnable(GL_TEXTURE_CUBE_MAP)

    skybox_right = [1, -1, -1, 1, -1,  1, 1,  1,  1, 1,  1,  1, 1,  1, -1, 1, -1, -1]
    skybox_left = [-1, -1,  1, -1, -1, -1, -1,  1, -1, -1,  1, -1, -1,  1,  1, -1, -1,  1]
    skybox_top = [-1,  1, -1, 1,  1, -1, 1,  1,  1, 1,  1,  1, -1,  1,  1, -1,  1, -1]
    skybox_bottom = [-1, -1, -1, -1, -1,  1, 1, -1, -1, 1, -1, -1, -1, -1,  1, 1, -1,  1]
    skybox_back = [-1,  1, -1, -1, -1, -1, 1, -1, -1, 1, -1, -1, 1,  1, -1, -1,  1, -1]
    skybox_front = [-1, -1,  1, -1,  1,  1, 1,  1,  1, 1,  1,  1, 1, -1,  1, -1, -1,  1]

    skybox_vertices = np.array([skybox_right, skybox_left, skybox_top, skybox_bottom, skybox_back, skybox_front], dtype=np.float32).flatten()
    skybox_vbo = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, skybox_vbo)
    glBufferData(GL_ARRAY_BUFFER, skybox_vertices.nbytes, skybox_vertices, GL_STATIC_DRAW)
    glBindBuffer(GL_ARRAY_BUFFER, 0)

    glClear(GL_COLOR_BUFFER_BIT)
    glClear(GL_DEPTH_BUFFER_BIT)

    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    gluPerspective(60, float(width)/height, 0.1, 1000)
    glMatrixMode(GL_MODELVIEW)
    glLoadIdentity()
    #glRotate(rotation, 0, 1, 0)#spin around y axis
    #glRotate(rotation, 1, 0, 0)#spin around x axis
    glRotate(rotation, 1, 1, 1)#rotate around x, y, and z axes

    glUseProgram(program)
    glDepthMask(GL_FALSE)
    glBindTexture(GL_TEXTURE_CUBE_MAP, cubemap)
    glEnableClientState(GL_VERTEX_ARRAY)
    glBindBuffer(GL_ARRAY_BUFFER, skybox_vbo)
    glVertexPointer(3, GL_FLOAT, 0, None)
    glDrawArrays(GL_TRIANGLES, 0, 36)
    glBindBuffer(GL_ARRAY_BUFFER, 0)
    glDisableClientState(GL_VERTEX_ARRAY)
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0)
    glDepthMask(GL_TRUE)
    glUseProgram(0)

    pygame.display.flip()

if __name__ == "__main__":
    title = "Skybox"
    target_fps = 60
    (width, height) = (800, 600)
    flags = pygame.DOUBLEBUF|pygame.OPENGL
    screen = pygame.display.set_mode((width, height), flags)
    prev_time = time.time()
    rotation = 0
    cubemap = load_cubemap("./images1/")#front and back images appear swapped
    #cubemap = load_cubemap("./images2/")#top and bottom images appear rotated by 90 and 270 degrees respectively
    #cubemap = load_cubemap("./images3/")#top and bottom images appear rotated by 180 degrees
    program = load_shaders("./shaders/skybox.vert", "./shaders/skybox.frag")
    pause = False

    while True:
        #Handle the events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE:
                    pause = not pause

        #Do computations and render stuff on screen
        if not pause:
            rotation += 1
        render()

        #Handle timing code for desired FPS
        curr_time = time.time()
        diff = curr_time - prev_time
        delay = max(1.0/target_fps - diff, 0)
        time.sleep(delay)
        fps = 1.0/(delay + diff)
        prev_time = curr_time
        pygame.display.set_caption("{0}: {1:.2f}".format(title, fps))

我使用以下顶点和片段着色器来显示天空盒的立方体贴图:

./shaders/skybox.vert

#version 120
varying vec3 tex_coords;

void main()
{
    gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
    tex_coords = vec3(gl_Vertex);
}

./shaders/skybox.frag

#version 120
varying vec3 tex_coords;
uniform samplerCube skybox;

void main()
{
    gl_FragColor = textureCube(skybox, tex_coords);
}

经过多次尝试,我认为错误出在 pygame 加载天空盒图像时。我测试了三组天空盒图像。每个都有不同的视觉错误和修复它们的技巧,我在上面的代码中已经注意到了这一点。以下是用于测试的三个天空盒的来源(请务必重命名图像,使其包含 rightlefttopbottomback , 或 front 在各自的文件名中)。

这三个天空盒都使用不同的图像格式(分别为 bmp、tga 和 png)。我如何才能在不依赖看似随机的旋转或图像交换的情况下,始终如一地稳健地处理所有这些和未来的图像案例?任何帮助或见解将不胜感激。

更新: 我创建了一个 github repository,您可以在其中测试代码,而无需创建 main.py 和着色器、下载图像以及自己重命名和组织内容。如果您有兴趣对其进行测试,这应该会使代码更容易 运行。

以下是我使用的所有版本:

如果您需要任何其他信息,请告诉我!

所以,事实证明,我在渲染天空盒时遇到的所有问题都可以归结为两个原因,none 其中 none 是由于 pygame 加载图像的方式不一致各种文件格式!

  1. 天空盒图像在立方体两个面之间的接缝连接方式上彼此不一致。这解释了为什么每个天空盒图像测试结果都有不同的问题。按照 this 问题中描述的位于立方体内部的惯例,我翻转并重新保存了绘画中的图像。
  2. 然而,仅此还不够。事实证明,OpenGL 在 "cubemap-land" 中对 z 轴的约定是 flipped。这导致正面和背面相互交换。我能想到的最简单的修复方法是在顶点着色器中交换纹理坐标。这是修正后的顶点着色器。

    #version 120
    varying vec3 tex_coords;
    
    void main()
    {
        gl_Position = gl_ProjectionMatrix * gl_ModelViewMatrix * gl_Vertex;
        tex_coords = vec3(gl_Vertex) * vec3(1, 1, -1);
    }
    

我有问题中提到的 github 中的代码来反映这些变化以及改进相机以手动环顾四周。

这是最终结果的 gif 动画,供所有感兴趣的人使用!