Implémentation d'une skybox avec GLSL version 330

14

J'essaie de faire fonctionner une skybox avec OpenGL 3.3 et GLSL version 330.

Je n'ai pas pu trouver de tutoriel OGL skybox complètement moderne n'importe où sur le web, j'ai donc modernisé un ancien (en utilisant à la glVertexAttribPointer()place de gl_Vertexpour les sommets, etc.). Cela fonctionne surtout, mais pour 2 détails majeurs:

Les skyboxes ressemblent plus à des triangles de ciel, et les textures sont mal déformées et étirées (elles sont censées être des champs d'étoiles, je reçois des lignes sur fond noir). Je suis sûr à 99% que c'est parce que je n'ai pas porté correctement les anciens tutoriels.

Voici ma classe 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);
}

Fragment Shader:

#version 330 compatibility

uniform samplerCube SkyTexture;

in vec3 Position;

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

Voici un exemple des problèmes. Si quelqu'un pouvait jeter un coup d'œil qui connaît bien GLSL (je l'apprends toujours) ou les skyboxes, j'apprécierais toute aide que vous pourriez apporter. Aussi, bravo si vous pouvez m'apprendre à utiliser des fonctions non obsolètes dans le fragment shader afin que je n'aie pas à utiliser le profil de compatibilité de glsl 330.


EDIT: J'ai immédiatement trouvé le problème avec les textures d'étirement: j'utilisais Position = Vertex.xyxplutôt que Position = Vertex.xyzdans le vertex shader. Oups. Mais l'erreur de triangle existe toujours.

sm81095
la source
1
Vous n'avez besoin que de 4 sommets (quadruple plein écran) pour rendre une skybox avec une texture cubemap. Vous avez juste besoin d'un vertex shader qui calcule les coordonnées de texture correctes en fonction de la caméra et de la projection.
msell
Ce pourrait être un problème d'abattage. Avez-vous essayé de désactiver la suppression de face arrière pour essayer de voir si vous obtenez la boîte complète?
pwny
@pwny, je n'y ai pas pensé. Je l'ai essayé, et cela n'a pas fonctionné, mais je peux voir comment cela a pu le rejeter. Merci pour la suggestion.
sm81095
@msell, j'ai entendu parler de cette approche, mais je n'ai pas trouvé de tutoriel en ligne pour cela, et je suis toujours en train d'apprendre glsl. Si vous pouviez fournir un exemple ou un lien vers un exemple sur la façon de procéder, je vous en serais très reconnaissant.
sm81095

Réponses:

29

Bien que cette réponse ne dise pas ce qui ne va pas avec votre approche, elle présente un moyen plus simple de rendre les skyboxes.

Façon traditionnelle (cube texturé)

Un moyen simple de créer des skyboxes est de rendre un cube texturé centré sur la position de la caméra. Chaque face du cube se compose de deux triangles et d'une texture 2D (ou d'une partie d'un atlas). En raison des coordonnées de texture, chaque face nécessite ses propres sommets. Cette approche a des problèmes dans les coutures des faces adjacentes, où les valeurs de texture ne sont pas interpolées correctement.

Cube avec texture cubemap

Comme à la manière traditionnelle, un cube texturé est rendu autour de la caméra. Au lieu d'utiliser six textures 2D, une seule texture cubemap est utilisée. Étant donné que la caméra est centrée à l'intérieur du cube, les coordonnées des sommets sont mappées une à une avec les vecteurs d'échantillonnage de la carte des cubes. Ainsi, les coordonnées de texture ne sont pas nécessaires pour les données de maillage et les sommets peuvent être partagés entre les faces à l'aide du tampon d'index.

Cette approche résout également le problème des coutures lorsque GL_TEXTURE_CUBE_MAP_SEAMLESS est activé.

Une manière plus simple (meilleure)

Lors du rendu d'un cube et de la caméra à l'intérieur, la fenêtre entière est remplie. Jusqu'à cinq faces de la skybox peuvent être partiellement visibles à tout moment. Les triangles des faces du cube sont projetés et découpés dans la fenêtre et les vecteurs d'échantillonnage de la carte du cube sont interpolés entre les sommets. Ce travail est inutile.

Il est possible de remplir un seul quadrilatère remplissant la fenêtre entière et de calculer les vecteurs d'échantillonnage cubemap dans les coins. Étant donné que les vecteurs d'échantillonnage de la carte cubique correspondent aux coordonnées des sommets, ils peuvent être calculés en supprimant la projection des coordonnées de la fenêtre dans l'espace mondial. Ceci est l'opposé de la projection des coordonnées du monde vers la fenêtre d'affichage et peut être obtenu en inversant les matrices. Assurez-vous également de désactiver l'écriture dans le tampon z ou d'écrire une valeur suffisamment éloignée.

Voici le vertex shader qui accomplit cela:

#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;
} 

aPositionest les coordonnées du sommet {-1,-1; 1,-1; 1,1; -1,1}. Le shader calculeeyeDirection avec l'inverse de la matrice modèle-vue-projection. Cependant, l'inversion est divisée pour les matrices de projection et du monde vers la caméra. En effet, seule la partie 3x3 de la matrice de la caméra doit être utilisée pour éliminer la position de la caméra. Cela aligne la caméra sur le centre de la skybox. De plus, comme mon appareil photo n'a pas d'échelle ni de cisaillement, l'inversion peut être simplifiée pour la transposition. L'inversion de la matrice de projection est une opération coûteuse et pourrait être précalculée, mais comme ce code est exécuté par le vertex shader généralement seulement quatre fois par image, ce n'est généralement pas un problème.

Le shader de fragment effectue simplement une recherche de texture en utilisant le eyeDirectionvecteur:

#version 330
uniform samplerCube uTexture;

smooth in vec3 eyeDirection;

out vec4 fragmentColor;

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

Notez que pour vous débarrasser du mode de compatibilité, vous devez remplacer textureCubepar juste textureet spécifier vous-même la variable de sortie.

msell
la source
Je pense que vous devriez également mentionner que l'inversion de matrice est un processus coûteux, donc elle se déroule mieux dans le code côté client.
akaltar
1
Pour les 4 verts d'un quad en plein écran, je ne pense pas que nous ayons besoin de nous inquiéter beaucoup du coût de l'inversion (d'autant plus que le GPU qui le fait 4 fois sera probablement plus rapide que le CPU qui le fait une fois).
Maximus Minimus
1
Juste une note utile aux gens, GLSL ES 1.0 (utilisé pour GL ES 2.0) ne met pas en œuvreinverse()
Steven Lu
uWorldToCameraMatrix est-il le MVP de l'objet de transformation de caméra?
Sidar
@Sidar Non, c'est juste la matrice ModelView, la projection est séparée.
msell