Implementierung einer Skybox mit GLSL Version 330

14

Ich versuche, eine Skybox mit OpenGL 3.3 und GLSL Version 330 zum Laufen zu bringen.

Ich konnte nirgendwo im Web ein vollständig modernes OGL-Skybox-Tutorial finden, also habe ich ein älteres modernisiert ( glVertexAttribPointer()statt gl_Vertexfür Vertices usw.). Es funktioniert meistens, aber für 2 wichtige Details:

Die Skyboxes sind eher wie Himmelsdreiecke, und die Texturen sind stark verzogen und gedehnt (sie sollen Sternfelder sein, ich bekomme dabei Linien auf schwarzem Hintergrund). Ich bin mir zu 99% sicher, dass dies daran liegt, dass ich die alten Tutorials nicht richtig portiert habe.

Hier ist meine Skybox-Klasse:

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

Hier ist ein Beispiel für die Störungen. Wenn jemand einen Blick auf GLSL (ich lerne es noch) oder Skyboxes werfen könnte, wäre ich für jede Hilfe dankbar. Außerdem ein großes Lob, wenn Sie mir beibringen können, wie man nicht veraltete Funktionen im Fragment-Shader verwendet, damit ich nicht das Kompatibilitätsprofil von glsl 330 verwenden muss.


EDIT: gefunden sofort das Problem mit den Strecken Texturen: Ich wurde mit Position = Vertex.xyxstatt Position = Vertex.xyzin den Vertex - Shader. Hoppla. Der Dreiecksfehler besteht jedoch weiterhin.

sm81095
quelle
1
Sie benötigen nur 4 Eckpunkte (Vollbild-Quad), um eine Skybox mit einer Cubemap-Textur zu rendern. Sie benötigen lediglich einen Vertex-Shader, der basierend auf Kamera und Projektion die korrekten Texturkoordinaten berechnet.
Msell
Es könnte ein Ausleseproblem sein. Haben Sie versucht, Backface-Culling zu deaktivieren, um zu sehen, ob Sie die volle Schachtel haben?
Pwny
@pwny, daran habe ich nicht gedacht. Ich habe es ausprobiert und es hat nicht funktioniert, aber ich kann sehen, wie das es hätte abwerfen können. Danke für den Vorschlag.
sm81095
@msell, ich habe von diesem Ansatz gehört, aber ich habe online kein Tutorial dafür gefunden und bin noch dabei, glsl zu lernen. Wenn Sie ein Beispiel oder einen Link zu einem Beispiel dazu bereitstellen könnten, würde ich das sehr begrüßen.
sm81095

Antworten:

29

Diese Antwort sagt zwar nicht, was an Ihrem Ansatz falsch ist, bietet jedoch eine einfachere Möglichkeit, Skyboxes zu rendern.

Traditionelle Art (strukturierter Würfel)

Eine einfache Möglichkeit zum Erstellen von Skyboxes ist das Rendern eines strukturierten Würfels in der Mitte der Kameraposition. Jede Fläche des Würfels besteht aus zwei Dreiecken und einer 2D-Textur (oder einem Teil eines Atlas). Aufgrund der Texturkoordinaten benötigt jede Fläche eigene Eckpunkte. Dieser Ansatz weist Probleme in den Nähten benachbarter Flächen auf, in denen die Texturwerte nicht richtig interpoliert werden.

Würfel mit Cubemap Textur

Wie auf herkömmliche Weise wird ein strukturierter Würfel um die Kamera herum gerendert. Anstelle von sechs 2D-Texturen wird eine einzelne Cubemap-Textur verwendet. Da die Kamera im Würfel zentriert ist, werden die Scheitelpunktkoordinaten eins zu eins mit den Abtastvektoren der Cubemap zugeordnet. Daher werden für die Netzdaten keine Texturkoordinaten benötigt und die Eckpunkte können mithilfe des Indexpuffers zwischen Flächen geteilt werden.

Dieser Ansatz behebt auch das Problem von Nähten, wenn GL_TEXTURE_CUBE_MAP_SEAMLESS aktiviert ist.

Einfacher (besser) Weg

Wenn Sie einen Würfel rendern und die Kamera darin liegt, wird das gesamte Ansichtsfenster gefüllt. Bis zu fünf Gesichter der Skybox können jederzeit teilweise sichtbar sein. Die Dreiecke der Würfelflächen werden projiziert und im Ansichtsfenster abgeschnitten, und die Stichprobenvektoren der Würfelkarte werden zwischen den Eckpunkten interpoliert. Diese Arbeit ist unnötig.

Es ist möglich, ein einzelnes Quad zu füllen, das das gesamte Ansichtsfenster ausfüllt, und die Cubemap-Abtastvektoren in den Ecken zu berechnen. Da die Abtastvektoren der Cubemap mit den Scheitelpunktkoordinaten übereinstimmen, können sie berechnet werden, indem die Ansichtsfensterkoordinaten in den Weltraum projiziert werden. Dies ist das Gegenteil der Projektion von Weltkoordinaten auf das Ansichtsfenster und kann durch Invertieren der Matrizen erreicht werden. Stellen Sie außerdem sicher, dass Sie entweder den Z-Buffer-Schreibvorgang deaktivieren oder einen Wert schreiben, der weit genug ist.

Unten sehen Sie den Vertex-Shader, mit dem dies erreicht wird:

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

aPositionist die Scheitelpunktkoordinaten {-1,-1; 1,-1; 1,1; -1,1}. Der Shader berechneteyeDirection mit der Umkehrung der Modellansicht-Projektionsmatrix. Die Inversion ist jedoch für Projektions- und Welt-zu-Kamera-Matrizen aufgeteilt. Dies liegt daran, dass nur der 3x3-Teil der Kameramatrix verwendet werden sollte, um die Position der Kamera zu beseitigen. Dadurch wird die Kamera auf die Mitte der Skybox ausgerichtet. Da meine Kamera keine Skalierung oder Scherung aufweist, kann die Inversion zur Transposition vereinfacht werden. Die Inversion der Projektionsmatrix ist eine kostspielige Operation und könnte vorberechnet werden. Da dieser Code jedoch vom Vertex-Shader in der Regel nur viermal pro Frame ausgeführt wird, ist dies normalerweise kein Problem.

Der Fragment-Shader führt einfach eine Textur-Suche mit eyeDirectionvector durch:

#version 330
uniform samplerCube uTexture;

smooth in vec3 eyeDirection;

out vec4 fragmentColor;

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

Beachten Sie , dass der Kompatibilitätsmodus , um loszuwerden , ersetzen müssen textureCubemit nur textureund geben Sie die Ausgangsgröße selbst.

msell
quelle
Ich denke, Sie sollten auch erwähnen, dass die Matrixinversion ein kostspieliger Prozess ist, so dass sie besser im clientseitigen Code stattfindet.
Altar
1
Für die 4 Verts eines Vollbild-Quaders brauchen wir uns meiner Meinung nach keine großen Sorgen um die Kosten der Inversion zu machen (zumal die GPU, die es 4-mal macht, wahrscheinlich immer noch schneller ist als die CPU, die es einmal macht).
Maximus Minimus
1
Nur ein hilfreicher Hinweis für die Leute, GLSL ES 1.0 (verwendet für GL ES 2.0) wird nicht implementiertinverse()
Steven Lu
Ist die uWorldToCameraMatrix das MVP des Kamera-Transformationsobjekts?
Sidar
@Sidar Nein, es ist nur die ModelView-Matrix. Die Projektion ist separat.
msell