Implementando uma skybox com GLSL versão 330

14

Estou tentando fazer com que um skybox funcione com o OpenGL 3.3 e GLSL versão 330.

Não consegui encontrar um tutorial do OGL skybox completamente moderno em nenhum lugar da Web; portanto, modernizei um mais antigo (usando em glVertexAttribPointer()vez de gl_Vertexvértices etc.). Está funcionando principalmente, mas com dois detalhes principais:

As caixas de céu são mais parecidas com triângulos do céu, e as texturas são muito distorcidas e esticadas (deveriam ser campos de estrelas, fico com linhas em fundo preto). Tenho 99% de certeza de que isso ocorre porque eu não mudei os tutoriais antigos completamente corretamente.

Aqui está a minha aula do skybox:

static ShaderProgram* cubeMapShader = nullptr;

static const GLfloat vertices[] = 
{
    1.0f, -1.0f,  1.0f,
    1.0f,  1.0f,  1.0f,
    1.0f,  1.0f, -1.0f,
    -1.0f, -1.0f,  1.0f,
    -1.0f, -1.0f, -1.0f,
    -1.0f,  1.0f, -1.0f,
    -1.0f,  1.0f,  1.0f,
    -1.0f,  1.0f, -1.0f,
    1.0f,  1.0f, -1.0f,
    1.0f,  1.0f,  1.0f,
    -1.0f,  1.0f,  1.0f,
    -1.0f, -1.0f,  1.0f,
    1.0f, -1.0f,  1.0f,
    1.0f, -1.0f, -1.0f,
    -1.0f, -1.0f, -1.0f,
    1.0f, -1.0f,  1.0f,
    -1.0f, -1.0f,  1.0f,
    -1.0f,  1.0f,  1.0f,
    1.0f,  1.0f,  1.0f,
    -1.0f, -1.0f, -1.0f,
    1.0f, -1.0f, -1.0f,
    1.0f,  1.0f, -1.0f,
    -1.0f,  1.0f, -1.0f
};

Skybox::Skybox(const char* xp, const char* xn, const char* yp, const char* yn, const        char* zp, const char* zn)
{
if (cubeMapShader == nullptr)
    cubeMapShader = new ShaderProgram("cubemap.vert", "cubemap.frag");

    texture = SOIL_load_OGL_cubemap(xp, xn, yp, yn, zp, zn, SOIL_LOAD_AUTO, SOIL_CREATE_NEW_ID, SOIL_FLAG_MIPMAPS);

    glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_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);

    glGenVertexArrays(1, &vaoID);
    glBindVertexArray(vaoID);
    glGenBuffers(1, &vboID);
    glBindBuffer(GL_ARRAY_BUFFER, vboID);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
    glBindVertexArray(0);

    scale = 1.0f;
}

Skybox::~Skybox()
{

}

void Skybox::Render()
{
    ShaderProgram::SetActive(cubeMapShader);
    glDisable(GL_DEPTH_TEST);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, texture);
    cubeMapShader->Uniform1i("SkyTexture", 0);
    cubeMapShader->UniformVec3("CameraPosition", Camera::ActiveCameraPosition());
    cubeMapShader->UniformMat4("MVP", 1, GL_FALSE, Camera::GetActiveCamera()->GetProjectionMatrix() * Camera::GetActiveCamera()->GetViewMatrix() * glm::mat4(1.0));
    glBindVertexArray(vaoID);
    glDrawArrays(GL_QUADS, 0, 24);
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
}

Vertex Shader:

#version 330 
layout(location = 0) in vec3 Vertex;

uniform vec3 CameraPosition;
uniform mat4 MVP;

out vec3 Position;

void main()
{
    Position = Vertex.xyz;
    gl_Position = MVP * vec4(Vertex.xyz + CameraPosition, 1.0);
}

Shader do fragmento:

#version 330 compatibility

uniform samplerCube SkyTexture;

in vec3 Position;

void main()
{
    gl_FragColor = textureCube(SkyTexture, Position);
}

Aqui está um exemplo das falhas. Se alguém puder dar uma olhada que conhece bem o GLSL (ainda estou aprendendo) ou caixas de sky, agradeceria qualquer ajuda que você pudesse dar. Além disso, parabéns, se você puder me ensinar como usar funções não obsoletas no shader de fragmentos, para que eu não precise usar o perfil de compatibilidade do glsl 330.


Edição: Encontrou imediatamente o problema com as texturas de alongamento: eu estava usando em Position = Vertex.xyxvez de Position = Vertex.xyzno shader de vértice. Opa Mas o erro do triângulo ainda existe.

sm81095
fonte
1
Você só precisa de 4 vértices (quad em tela cheia) para renderizar uma skybox com uma textura de mapa de cubo. Você só precisa de um sombreador de vértice que calcule as coordenadas de textura corretas com base na câmera e na projeção.
msell
Pode ser um problema de seleção. Você já tentou desativar o descarte do backface para tentar ver se recebeu a caixa completa?
Pwny #
@ pwny, eu não pensei nisso. Eu tentei, e não funcionou, mas posso ver como isso poderia ter acontecido. Obrigado pela sugestão.
sm81095
@msell, ouvi falar dessa abordagem, mas não encontrei um tutorial on-line para isso, e ainda estou no processo de aprender glsl. Se você pudesse fornecer um exemplo ou um link para um exemplo de como fazer isso, eu agradeceria muito.
precisa saber é

Respostas:

29

Embora essa resposta não diga o que há de errado com sua abordagem, ela apresenta uma maneira mais simples de renderizar caixas de correio.

Maneira tradicional (cubo texturizado)

Uma maneira simples de criar skyboxes é renderizar um cubo texturizado centralizado na posição da câmera. Cada face do cubo consiste em dois triângulos e uma textura 2D (ou parte de um atlas). Devido às coordenadas da textura, cada face requer vértices próprios. Essa abordagem apresenta problemas nas costuras das faces adjacentes, onde os valores da textura não são interpolados adequadamente.

Cubo com textura de mapa de cubo

Como na maneira tradicional, um cubo texturizado é renderizado em torno da câmera. Em vez de usar seis texturas 2D, é usada uma única textura de mapa de cubo. Como a câmera está centralizada dentro do cubo, as coordenadas do vértice são mapeadas uma a uma com os vetores de amostragem do mapa do cubo. Portanto, as coordenadas de textura não são necessárias para os dados da malha e os vértices podem ser compartilhados entre as faces usando o buffer de índice.

Essa abordagem também corrige o problema de costuras quando GL_TEXTURE_CUBE_MAP_SEAMLESS está ativado.

Maneira mais simples (melhor)

Ao renderizar um cubo e a câmera ficar dentro dele, toda a janela de exibição é preenchida. Até cinco faces da caixa do céu podem ser parcialmente visíveis a qualquer momento. Os triângulos das faces do cubo são projetados e cortados na viewport e os vetores de amostragem do mapa do cubo são interpolados entre os vértices. Este trabalho é desnecessário.

É possível preencher um único quad preenchendo toda a viewport e calcular os vetores de amostragem do mapa do cubo nos cantos. Como os vetores de amostragem do mapa do cubo correspondem às coordenadas do vértice, eles podem ser calculados desprojetando as coordenadas da viewport para o espaço do mundo. É o oposto de projetar coordenadas mundiais para a viewport e pode ser conseguido invertendo as matrizes. Além disso, certifique-se de desabilitar a gravação do buffer z ou gravar um valor suficientemente longe.

Abaixo está o vertex shader que realiza isso:

#version 330
uniform mat4 uProjectionMatrix;
uniform mat4 uWorldToCameraMatrix;

in vec4 aPosition;

smooth out vec3 eyeDirection;

void main() {
    mat4 inverseProjection = inverse(uProjectionMatrix);
    mat3 inverseModelview = transpose(mat3(uWorldToCameraMatrix));
    vec3 unprojected = (inverseProjection * aPosition).xyz;
    eyeDirection = inverseModelview * unprojected;

    gl_Position = aPosition;
} 

aPositionsão as coordenadas do vértice {-1,-1; 1,-1; 1,1; -1,1}. O sombreador calcula eyeDirectioncom o inverso da matriz modelo-vista-projeção. No entanto, a inversão é dividida para matrizes de projeção e do mundo para a câmera. Isso ocorre porque apenas a parte 3x3 da matriz da câmera deve ser usada para eliminar a posição da câmera. Isso alinha a câmera ao centro da caixa do céu. Além disso, como minha câmera não possui redimensionamento ou cisalhamento, a inversão pode ser simplificada para transposição. A inversão da matriz de projeção é uma operação dispendiosa e pode ser pré-calculada, mas como esse código é executado pelo shader de vértice normalmente apenas quatro vezes por quadro, geralmente não é um problema.

O shader de fragmento simplesmente executa uma pesquisa de textura usando o eyeDirectionvetor:

#version 330
uniform samplerCube uTexture;

smooth in vec3 eyeDirection;

out vec4 fragmentColor;

void main() {
    fragmentColor = texture(uTexture, eyeDirection);
}

Observe que, para se livrar do modo de compatibilidade, você precisa substituir textureCubepor just texturee especificar a variável de saída por conta própria.

msell
fonte
Eu acho que você também deve mencionar que a inversão da matriz é um processo caro, por isso ocorre melhor no código do lado do cliente.
Akaltar
1
Para os 4 verts de um quad de tela cheia, não acho que precisamos nos preocupar muito com o custo da inversão (especialmente porque a GPU faz isso 4 vezes ainda provavelmente será mais rápida do que a CPU fazendo isso uma vez).
Maximus Minimus
1
Apenas uma observação útil para as pessoas, o GLSL ES 1.0 (usado para o GL ES 2.0) não implementainverse()
Steven Lu
o uWorldToCameraMatrix é o MVP do objeto de transformação da câmera?
Sidar
@Sidar Não, é apenas a matriz ModelView, a projeção é separada.
msell